spring-cloud-gateway || need to configure global and application level and api level timeout - spring-boot

I am working in a spring-cloud-gateway project. Where I need to configure a Global Timeout/ application level timeout and api specific timeout. Following are my downstream apis:
#RestController
public class TestController {
// Should have global time out
#GetMapping("/global")
public Mono<ResponseEntity> testGlobalTimeOut(
#RequestHeader(name = "cId") UUID cId,
#RequestParam(name = "someNumber", required = false) Number someNumber) {
// Map<String, Object> map = populate Some Map Logic
return Mono.just(new ResponseEntity(map, HttpStatus.OK));
}
// Should have application level time out
#GetMapping("/appname/count")
public Mono<ResponseEntity> testApplicationTimeOut_1(
#RequestHeader(name = "cId") UUID cId,
#RequestParam(name = "someNumber", required = false) Number someNumber) {
// Map<String, Object> map = populate Some Map Logic
return Mono.just(new ResponseEntity(map, HttpStatus.OK));
}
// Should have application level time out
#GetMapping("/appname/posts")
public Mono<ResponseEntity> testApplicationTimeOut_2(
#RequestHeader(name = "cId") UUID cId,
#RequestParam(name = "someNumber", required = false) Number someNumber) {
// Map<String, Object> map = populate Some Map Logic
return Mono.just(new ResponseEntity(map, HttpStatus.OK));
}
// Should have api level time out
#GetMapping("/appname/posts/{postId}")
public Mono<ResponseEntity> getAPITimeOutWithPathVariable(
#RequestHeader(name = "cId") UUID cId,
#PathVariable(name = "postId") String postId) {
// Map<String, Object> map = populate Some Map Logic
return Mono.just(new ResponseEntity(map, HttpStatus.OK));
}
}
This apis are running as a downstream service. Now following are my route configurations for all these apis in my gateway-application:
# ============ Application Timeout =============
- id: application_timeout_route_1
uri: http://localhost/appname/count
predicates:
- Path=/withapplicationtimeout1**
filters:
- Hystrix=appTimeOut
- id: application_timeout_route_2
uri: http://localhost/appname/posts
predicates:
- Path=/withapplicationtimeout2**
filters:
- Hystrix=appTimeOut
# ============ API Level Timeout ===========
- id: api_timeout_route
uri: http://localhost
predicates:
- Path=/withapitimeout/**
filters:
- Hystrix=apiTimeOut
- RewritePath=/withapitimeout/(?<segment>.*), /appname/posts/$\{segment}
# Global Timeout Configuration
#hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 30000
# Application Level Timeout Configuration
hystrix.command.appTimeOut.execution.isolation.thread.timeoutInMilliseconds: 30000
# API Level Timeout Configuration
hystrix.command.apiTimeOut.execution.isolation.thread.timeoutInMilliseconds: 15000
Now the application level timeout and the api level timeout is working fine, But I am not getting any way to define the Global Timeout filter. Documentation for the same is yet not available:
https://github.com/spring-cloud/spring-cloud-gateway/blob/master/docs/src/main/asciidoc/spring-cloud-gateway.adoc#combined-global-filter-and-gatewayfilter-ordering
Any Idea how to do this?

Related

Reuse existing token rather than requesting it on every request in spring boot + Retrofit app

I have a spring boot application that uses Retrofit to make requests to a secured server.
My endpoints:
public interface ServiceAPI {
#GET("/v1/isrcResource/{isrc}/summary")
Call<ResourceSummary> getResourceSummaryByIsrc(#Path("isrc") String isrc);
}
public interface TokenServiceAPI {
#FormUrlEncoded
#POST("/bbcb6b2f-8c7c-4e24-86e4-6c36fed00b78/oauth2/v2.0/token")
Call<Token> obtainToken(#Field("client_id") String clientId,
#Field("scope") String scope,
#Field("client_secret") String clientSecret,
#Field("grant_type") String grantType);
}
Configuration class:
#Bean
Retrofit tokenAPIFactory(#Value("${some.token.url}") String tokenUrl) {
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(tokenUrl)
.addConverterFactory(JacksonConverterFactory.create());
return builder.build();
}
#Bean
Retrofit serviceAPIFactory(#Value("${some.service.url}") String serviceUrl, TokenServiceAPI tokenAPI) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new ServiceInterceptor(clientId, scope, clientSecret, grantType, apiKey, tokenAPI))
.build();
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(repertoireUrl)
.client(okHttpClient)
.addConverterFactory(JacksonConverterFactory.create());
return builder.build();
}
Interceptor to add the Authorization header to every request
public class ServiceInterceptor implements Interceptor {
public ServiceInterceptor(String clientId,
String scope,
String clientSecret,
String grantType,
String apiKey,
TokenServiceAPI tokenAPI) {
this.clientId = clientId;
this.scope = scope;
this.clientSecret = clientSecret;
this.grantType = grantType;
this.apiKey = apiKey;
this.tokenAPI = tokenAPI;
}
#Override
public Response intercept(Chain chain) throws IOException {
Request newRequest = chain.request().newBuilder()
.addHeader(AUTHORIZATION_HEADER, getToken())
.addHeader(API_KEY_HEADER, this.apiKey)
.build();
return chain.proceed(newRequest);
}
private String getToken() throws IOException {
retrofit2.Response<Token> tokenResponse = repertoireTokenAPI.obtainToken(clientId, scope, clientSecret, grantType).execute();
String accessToken = "Bearer " + tokenAPI.body().getAccessToken();
return accessToken;
}
}
This is working as expected, the problem is that the token is being requested for every request rather than using the existing valid one. How can one store the token somewhere and re-use it? I was wondering if Retrofit had a built-in solution.
a possible option with caching:
add caffeiene
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
add #Cacheable("your-token-cache-name") on the method returning the token, looks like getToken above
add max cache size and expiration configuration in application.yml
e.g. 500 entries and 10 minutes for configuration below
spring.cache.cache-names=your-token-cache-name
spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s
example from: https://www.javadevjournal.com/spring-boot/spring-boot-with-caffeine-cache/

Rate limit of the Rest api using spring cloud gateway does not work

I tried to run, but I don’t understand that why don’t show an error when to occur rate limit to rest api.
GatewaySecureRateLimiterTest - I only observe the success of requests, but I do not see errors when limiting requests.
In addition, I would like to clarify for myself (I did not find it in the documentation):
I would like to clarify for myself (I didn't find this in the documentation):
how to change the error code to the address to which the response was sent that the resource is currently busy
is it possible to collect statistics and see which IP generates more requests than our endpoint can handle
the speed limit can only be configured using *. yml, or it can also be configured using Java, while ?
I would like to see it in tests. what is the restriction-it triggers and gets detailed information(for example, from which IP and how many requests and in what unit of time).
I also didn't fully understand what the meaning of these parameters is:
key determinant: "#{#userRemoteAddressResolver}"
reuse rate limiter.Top-up rate: 1
reuse rate limiter.Bandwidth: 1
For example, I would like to know what is the number of requests in these parameters, and what is the unit of time during which this number of requests should work ?
server:
port: ${PORT:8085}
logging.pattern.console: "%clr(%d{HH:mm:ss.SSS}){blue} %clr(---){faint} %clr([%15.15t]){yellow} %clr(:){red} %clr(%m){faint}%n"
spring:
application:
name: gateway-service
redis:
host: 192.168.99.100
port: 6379
output.ansi.enabled: ALWAYS
cloud:
gateway:
routes:
- id: account-service
uri: http://localhost:8085
predicates:
- Path=/account/**
filters:
- RewritePath=/account/(?<path>.*), /$\{path}
- name: RequestRateLimiter
args:
key-resolver: "#{#userKeyResolver}"
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
config-security
#Configuration
//#ConditionalOnProperty("rateLimiter.secure")
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange(exchanges ->
exchanges
.anyExchange()
.authenticated())
.httpBasic();
http.csrf().disable();
return http.build();
}
#Bean
public MapReactiveUserDetailsService users() {
UserDetails user1 = User.builder()
.username("user1")
.password("{noop}1234")
.roles("USER")
.build();
UserDetails user2 = User.builder()
.username("user2")
.password("{noop}1234")
.roles("USER")
.build();
UserDetails user3 = User.builder()
.username("user3")
.password("{noop}1234")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user1, user2, user3);
}
}
config
#Configuration
public class GatewayConfig {
#Bean
#Primary
#ConditionalOnProperty("rateLimiter.non-secure")
KeyResolver userKeyResolver() {
return exchange -> Mono.just("1");
}
// #Bean
// #ConditionalOnProperty("rateLimiter.secure")
KeyResolver authUserKeyResolver() {
return exchange -> ReactiveSecurityContextHolder.getContext()
.map(securityContext -> securityContext.getAuthentication()
.getPrincipal()
.toString()
);
}
}
test
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
properties = {"rateLimiter.non-secure=true"})
#RunWith(SpringRunner.class)
public class GatewayRateLimiterTest {
private static final Logger LOGGER =
LoggerFactory.getLogger(GatewayRateLimiterTest.class);
private Random random = new Random();
#Rule
public TestRule benchmarkRun = new BenchmarkRule();
private static final DockerImageName IMAGE_NAME_MOCK_SERVER =
DockerImageName.parse("jamesdbloom/mockserver:mockserver-5.11.2");
#ClassRule
public static MockServerContainer mockServer =
new MockServerContainer(IMAGE_NAME_MOCK_SERVER);
#ClassRule
public static GenericContainer redis =
new GenericContainer("redis:5.0.6")
.withExposedPorts(6379);
#Autowired
TestRestTemplate testRestTemplate;
#Test
#BenchmarkOptions(warmupRounds = 0, concurrency = 6, benchmarkRounds = 600)
public void testAccountService() {
String username = "user" + (random.nextInt(3) + 1);
HttpHeaders headers = createHttpHeaders(username,"1234");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Account> responseEntity =
testRestTemplate.exchange("/account/{id}",
HttpMethod.GET,
entity,
Account.class,
1);
LOGGER.info("Received: status->{}, payload->{}, remaining->{}",
responseEntity.getStatusCodeValue(),
responseEntity.getBody(),
responseEntity.getHeaders()
.get("X-RateLimit-Remaining"));
}
private HttpHeaders createHttpHeaders(String user, String password) {
String notEncoded = user + ":" + password;
String encodedAuth = Base64.getEncoder().encodeToString(notEncoded.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("Authorization", "Basic " + encodedAuth);
return headers;
}
}
repository
here

Retaining the Request's Path During Spring Cloud Gateway Failover

Is there a way to externally configure Spring Cloud Gateway to failover to another data center? I'm thinking of something like this:
spring:
cloud:
gateway:
routes:
- id: test-service
uri: lb://test-service:8085/
predicates:
- Path=/test-service/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: fallback
fallbackUri: forward:/fallback
#fallbackUri: forward:/fallback/test-service
- id: fallback
uri: http://${fallback_data_center}
predicates:
- Path=/fallback/**
---
spring:
config:
activate:
on-profile: data_center_1
fallback_data_center: dc2.com
---
spring:
config:
activate:
on-profile: data_center_2
fallback_data_center: dc1.com
The problem I run into is that the CircuitBreaker filter's fallbackUri parameter only supports forward schemed URIs. However, the path part of the request URL is overridden with the path in the forward URL. So there does not appear to be a way to failover with the path from the original request such as if this configuration had received a request of http://dc1.com/test-service/some/path without creating a configuration for every possible path.
At the time of writing this answer there is still now official way of doing a failover to another host.
What we are trying to achieve in our team is to have routes with Retry and CircuitBreaker filters which can fallback to another host keeping the original request unmodified ( request payload, header, query params and most importantly the API context path ) and just replacing the host so we can fallback to another datacenter.
We archived this by using the default Gateway Retry and CircuitBreaker filters and developing a custom FallbackController which just replaces the host with a configured property and keeps the rest of the request unmodified including the request context path:
#RestController
#RequestMapping("/fallback")
#ConditionalOnProperty(value="gateway.fallback.enabled", havingValue = "true")
public class FallbackController {
private final GatewayFallbackConfig gatewayFallbackConfig;
private final WebClient webClient;
public FallbackController(GatewayFallbackConfig gatewayFallbackConfig) {
this.gatewayFallbackConfig = gatewayFallbackConfig;
this.webClient = WebClient.create();
}
#PostMapping
Mono<ResponseEntity<String>> postFallback(#RequestBody(required = false) String body,
ServerWebExchangeDecorator serverWebExchangeDecorator) {
return fallback(body, serverWebExchangeDecorator);
}
#GetMapping
Mono<ResponseEntity<String>> getFallback(#RequestBody(required = false) String body,
ServerWebExchangeDecorator serverWebExchangeDecorator) {
return fallback(body, serverWebExchangeDecorator);
}
#PatchMapping
Mono<ResponseEntity<String>> patchFallback(#RequestBody(required = false) String body,
ServerWebExchangeDecorator serverWebExchangeDecorator) {
return fallback(body, serverWebExchangeDecorator);
}
#DeleteMapping
Mono<ResponseEntity<String>> deleteFallback(#RequestBody(required = false) String body,
ServerWebExchangeDecorator serverWebExchangeDecorator) {
return fallback(body, serverWebExchangeDecorator);
}
private Mono<ResponseEntity<String>> fallback(String body, ServerWebExchangeDecorator serverWebExchangeDecorator) {
ServerHttpRequest originalRequest = serverWebExchangeDecorator.getDelegate().getRequest();
WebClient.RequestBodySpec request = webClient.method(originalRequest.getMethod())
.uri(buildFallbackURI(originalRequest));
Optional.ofNullable(body)
.ifPresent(request::bodyValue);
return request.exchangeToMono(response -> response.toEntity(String.class));
}
private URI buildFallbackURI(ServerHttpRequest originalRequest) {
return UriComponentsBuilder.fromHttpRequest(originalRequest)
.scheme(gatewayFallbackConfig.getScheme())
.host(gatewayFallbackConfig.getHost())
.port(gatewayFallbackConfig.getPort())
.build(ServerWebExchangeUtils.containsEncodedParts(originalRequest.getURI()))
.toUri();
}
With an additional property configuration holder:
#Getter
#Component
#RefreshScope
#ConditionalOnProperty(value="gateway.fallback.enabled", havingValue = "true")
public class GatewayFallbackConfig {
private final String scheme;
private final String host;
private final String port;
private final Set<String> excludedHeaders;
public GatewayFallbackConfig(
#Value("${gateway.fallback.scheme:https}") String scheme,
#Value("${gateway.fallback.host}") String host,
#Value("${gateway.fallback.port:#{null}}") String port,
#Value("${gateway.fallback.headers.exclude}") Set<String> excludedHeaders) {
this.scheme = scheme;
this.host = host;
this.port = port;
this.excludedHeaders = excludedHeaders;
}
And we are using it with a route configuration like that:
- id: example-route
uri: http://localhost:8080
predicates:
- Path=/foo/bar/**
filters:
- name: CircuitBreaker
args:
name: exampleCircuitBreaker
fallbackUri: forward:/fallback
statusCodes:
- INTERNAL_SERVER_ERROR
- BAD_GATEWAY
- SERVICE_UNAVAILABLE
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
series: SERVER_ERROR
methods: GET,POST,PUT,DELETE
exceptions: org.springframework.cloud.gateway.support.NotFoundException,javax.security.auth.login.LoginException
backoff:
firstBackoff: 10ms
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
gateway:
fallback:
scheme: https
host: some.other.host.com
enabled: true

Set cache expireAfterWrite property dynamically - Caffeine and Spring WebFlux

I am using caffeine cache to store an authorisation token that has been obtained using webClient WebFlux. I have set the expireAfterWrite to a hardcoded value in the application.yml file as follows:
spring:
cache:
cache-names: accessTokens
caffeine:
spec: expireAfterWrite=100m
The token is obtained using a WebClient with Spring WebFlux as below code depicts:
#Autowired
var cacheManager: CacheManager? = null
override fun getAuthToken(): Mono<AccessToken> {
val map = LinkedMultiValueMap<String, String>()
map.add("client_id", clientId)
map.add("client_secret", clientSecret)
map.add("grant_type", "client_credentials")
var cachedVersion = this.cacheManager?.getCache("accessTokens");
if (cachedVersion?.get("tokens") != null) {
val token = cachedVersion.get("tokens")
return Mono.just(token?.get() as AccessToken)
} else {
return webClient.post()
.uri("/client-credentials/token")
.body(BodyInserters.fromFormData(map))
.retrieve()
.onStatus(HttpStatus::is5xxServerError) {
ClientLogger.logClientErrorResponse(it, errorResponse)
}
.onStatus(HttpStatus::is4xxClientError) {
ClientLogger.logClientErrorResponse(it, errorResponse)
}
.bodyToMono(AccessToken::class.java)
.doOnNext { response ->
// Set here the expiration time of the cache based on
// response.expiresIn
this.cacheManager?.getCache("accessTokens")?.put("tokens", response) }
.log()
}
}
I am storing the token after the data is emitted/returned successfully within the .doOnNext() method but i need to be able to set the expiration time or refresh the hardcoded expiration time of the cache based on the expiresIn property that is part of the response object,
.doOnNext { response ->
// Set here the expiration time of the cache based on
// response.expiresIn
this.cacheManager?.getCache("accessTokens")?.put("tokens", response)
}
Any ideas would be much appreciated.
// Policy to set the lifetime based on when the entry was created
var expiresAfterCreate = new Expiry<String, AccessToken>() {
public long expireAfterCreate(String credentials, AccessToken token, long currentTime) {
Duration duration = token.expiresIn();
return token.toNanos();
}
public long expireAfterUpdate(String credentials, AccessToken token,
long currentTime, long currentDuration) {
return currentDuration;
}
public long expireAfterRead(String credentials, AccessToken token,
long currentTime, long currentDuration) {
return currentDuration;
}
});
// CompletableFuture-based cache
AsyncLoadingCache<String, AccessToken> cache = Caffeine.newBuilder()
.expireAfter(expiresAfterCreate)
.buildAsync((credentials, executor) -> {
Mono<AccessToken> token = retrieve(credentials);
return token.toFuture();
});
// Get from cache, loading if absent, and converts to Mono
Mono<AccessToken> getAuthToken() {
var token = cache.get(credentials);
return Mono.fromFuture(token);
}

How to use ReadBodyPredicateFactory to cache payload data

I have Spring Boot microservice, and sending large payload using swagger. At the server I get only 15000 chars and reset 2000 chars are not read.
How can I use ReadBodyPredicateFactory to cache the body message text?
I am using springcloudgateway and added filters. In the filter in apply method I am trying to read the payload json using
DefaultServerRequest serverRequest = new DefaultServerRequest(exchange);
body = serverRequest.bodyToMono(String.class).toFuture().get();
Sometimes it hangs.
I tried with Flux and then i get only half message
Flux body = request.getBody();
body.subscribe(buffer -> {
try {
System.out.println("byte count:" +
buffer.readableByteCount());
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
String bodyString = new String(bytes, StandardCharsets.UTF_8);
sb.append(bodyString);
} catch (Exception e) {
e.printStackTrace();
}
Recently, I needed the similar thing in my application and I've found that it can be achieved by Spring Cloud Gateway built-in caching in ServerWebExchangeUtils
Before filters that use request content in some business cases, I created a filter that only forces content caching:
#Component
class CachingRequestBodyFilter extends AbstractGatewayFilterFactory<CachingRequestBodyFilter.Config> {
public CachingRequestBodyFilter() {
super(Config.class);
}
public GatewayFilter apply(final Config config) {
return (exchange, chain) -> ServerWebExchangeUtils.cacheRequestBody(exchange,
(serverHttpRequest) -> chain.filter(exchange.mutate().request(serverHttpRequest).build()));
}
public static class Config {
}
}
In any of the subsequent filters, we can extract the content of the request body, as below:
// some ReadRequestBodyFilter filter
public GatewayFilter apply(final Config config) {
return (exchange, chain) -> {
final var cachedBody = new StringBuilder();
final var cachedBodyAttribute = exchange.getAttribute(CACHED_REQUEST_BODY_ATTR);
if (!(cachedBodyAttribute instanceof DataBuffer)) {
// caching gone wrong error handling
}
final var dataBuffer = (DataBuffer) cachedBodyAttribute;
cachedBody.append(StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()).toString());
final var bodyAsJson = cachedBody.toString();
// some processing
return chain.filter(exchange);
};
}
Then the gateway configuration would look like this:
spring:
cloud:
gateway:
routes:
- [...]
filters:
- CachingRequestBodyFilter
- ReadRequestBodyFilter

Resources