Spring Cloud Gateway, logging request/response - spring-boot

My GlobalFilter only logs successful requests (200). For example, code 500 does not pass through the ServerHttpRequestDecorator and ServerHttpResponseDecorator.
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequestDecorator requestMutated = new ServerHttpRequestDecorator(exchange.getRequest()) {
#Override
public Flux<DataBuffer> getBody() {
Logger requestLogger = new Logger(getDelegate());
if(LOGGABLE_CONTENT_TYPES.contains(String.valueOf(getHeaders().getContentType()).toLowerCase())) {
return super.getBody().map(ds -> {
requestLogger.appendBody(ds.asByteBuffer());
return ds;
}).doFinally(s -> requestLogger.log());
} else {
requestLogger.log();
return super.getBody();
}
}
};
ServerHttpResponseDecorator responseMutated = new ServerHttpResponseDecorator(exchange.getResponse()) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
Logger responseLogger = new Logger(getDelegate());
if(LOGGABLE_CONTENT_TYPES.contains(String.valueOf(getHeaders().getContentType()).toLowerCase())) {
return join(body).flatMap(db -> {
responseLogger.appendBody(db.asByteBuffer());
responseLogger.log();
return getDelegate().writeWith(Mono.just(db));
});
} else {
responseLogger.log();
return getDelegate().writeWith(body);
}
}
};
return chain.filter(exchange.mutate().request(requestMutated).response(responseMutated).build());
ServerResponse.status(HttpStatus.valueOf(statusCode)) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(errorPropertiesMap)) => not log request and response;
What am I doing wrong? thanks for the help.

Try enable these properties in application.yml:
logging:
level:
org.springframework.web.HttpLogging: TRACE
reactor.netty.http.server: DEBUG
reactor.netty.http.client: DEBUG

Related

JwtAuthenticationFilter Junit Testcases

#Component
#Slf4j
public class JwtAuthenticationFilter implements GatewayFilter {
#Autowired
private JwtUtil jwtUtil;
#Override
public Mono<Void> filter(final ServerWebExchange exchange,
final GatewayFilterChain chain) {
log.info("Start --> filter()");
ServerHttpRequest request = (ServerHttpRequest) exchange.getRequest();
if (!request.getHeaders().containsKey("Authorization")) {
ServerHttpResponse response = exchange.getResponse();
log.debug("response status {}", response.getStatusCode());
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
final String token = request.getHeaders().getOrEmpty("Authorization").get(0);request = {ReactorServerHttpRequest#12622}
try {
jwtUtil.validateToken(token);
} catch (JwtTokenMalformedException | JwtTokenMissingException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.BAD_REQUEST);
log.debug("response status {}", response.getStatusCode());
return response.setComplete();
}
Claims claims = jwtUtil.getClaims(token);
exchange.getRequest().mutate().header("id", String.valueOf(claims.get("id"))).build();
log.info("end filter()");
return chain.filter(exchange);
}
}
can someone please explain me how to write junits for this. I am very much new to this Junits and i tried in google also, but could not find the how to check if conditions using Junit/Mockito

Spring Cloud Gateway filter read and modify response body

I am trying to read and modify the response body in the filter. I have added a custom filter to read and modify the changes but didn't found any option to read the body part. I can change the headers and cookie but not the body of the response
#Configuration
public class GatewayConfiguration {
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder, CustomGatewayFilterFactory extractFilter) {
return builder.routes()
.route("ccprest",r -> r.path("/api/details/**").
filters( f -> f.filter(extractFilter.apply(new ExtractCCPUrlGatewayFilterFactory.Config()))).uri(http://localhost:8090/test))
.build();
}
Custom filter
#CommonsLog
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory< CustomGatewayFilterFactory.Config> {
protected static final ObjectMapper MAPPER = new ObjectMapper();
public CustomGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter((exchange, chain) -> {
return chain.filter(exchange.mutate().request(request).build()).s
then(
Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
//response.getBody() //Dont know how to read and modify the body
Optional.ofNullable(exchange.getRequest()
.getQueryParams()
.getFirst("include-total"))
.ifPresent(qp -> {
String responseContentLanguage = "hai";
response.getHeaders()
.add("Bael-Custom-Language-Header", responseContentLanguage);
});
}));
}, 10);
}
public static class Config {
}
}

Spring cloud gateway with Spring cache and caffeine

I have a spring cloud gateway which forwards the API rest requests to some microservices.
I would like to cache the response for specific requests.
For this reason I wrote this Filter
#Component
#Slf4j
public class CacheResponseGatewayFilterFactory extends AbstractGatewayFilterFactory<CacheResponseGatewayFilterFactory.Config> {
private final CacheManager cacheManager;
public CacheResponseGatewayFilterFactory(CacheManager cacheManager) {
super(CacheResponseGatewayFilterFactory.Config.class);
this.cacheManager = cacheManager;
}
#Override
public GatewayFilter apply(CacheResponseGatewayFilterFactory.Config config) {
final var cache = cacheManager.getCache("MyCache");
return (exchange, chain) -> {
final var path = exchange.getRequest().getPath();
if (nonNull(cache.get(path))) {
log.info("Return cached response for request: {}", path);
final var response = cache.get(path, ServerHttpResponse.class);
final var mutatedExchange = exchange.mutate().response(response).build();
return mutatedExchange.getResponse().setComplete();
}
return chain.filter(exchange).doOnSuccess(aVoid -> {
cache.put(path, exchange.getResponse());
});
};
}
When I call my rest endpoint, the first time I receive the right json, the second time I got an empty body.
What am I doing wrong?
EDIT
This is a screenshot of the exchange.getRequest() just before doing cache.put()
I solved it creating a GlobalFilter and a ServerHttpResponseDecorator. This code is caching all the responses regardless (it can be easily improved to cache only specific responses).
This is the code. However I think it can be improved. In case let me know.
#Slf4j
#Component
public class CacheFilter implements GlobalFilter, Ordered {
private final CacheManager cacheManager;
public CacheFilter(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
final var cache = cacheManager.getCache("MyCache");
final var cachedRequest = getCachedRequest(exchange.getRequest());
if (nonNull(cache.get(cachedRequest))) {
log.info("Return cached response for request: {}", cachedRequest);
final var cachedResponse = cache.get(cachedRequest, CachedResponse.class);
final var serverHttpResponse = exchange.getResponse();
serverHttpResponse.setStatusCode(cachedResponse.httpStatus);
serverHttpResponse.getHeaders().addAll(cachedResponse.headers);
final var buffer = exchange.getResponse().bufferFactory().wrap(cachedResponse.body);
return exchange.getResponse().writeWith(Flux.just(buffer));
}
final var mutatedHttpResponse = getServerHttpResponse(exchange, cache, cachedRequest);
return chain.filter(exchange.mutate().response(mutatedHttpResponse).build());
}
private ServerHttpResponse getServerHttpResponse(ServerWebExchange exchange, Cache cache, CachedRequest cachedRequest) {
final var originalResponse = exchange.getResponse();
final var dataBufferFactory = originalResponse.bufferFactory();
return new ServerHttpResponseDecorator(originalResponse) {
#NonNull
#Override
public Mono<Void> writeWith(#NonNull Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
final var flux = (Flux<? extends DataBuffer>) body;
return super.writeWith(flux.buffer().map(dataBuffers -> {
final var outputStream = new ByteArrayOutputStream();
dataBuffers.forEach(dataBuffer -> {
final var responseContent = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(responseContent);
try {
outputStream.write(responseContent);
} catch (IOException e) {
throw new RuntimeException("Error while reading response stream", e);
}
});
if (Objects.requireNonNull(getStatusCode()).is2xxSuccessful()) {
final var cachedResponse = new CachedResponse(getStatusCode(), getHeaders(), outputStream.toByteArray());
log.debug("Request {} Cached response {}", cacheKey.getPath(), new String(cachedResponse.getBody(), UTF_8));
cache.put(cacheKey, cachedResponse);
}
return dataBufferFactory.wrap(outputStream.toByteArray());
}));
}
return super.writeWith(body);
}
};
}
#Override
public int getOrder() {
return -2;
}
private CachedRequest getCachedRequest(ServerHttpRequest request) {
return CachedRequest.builder()
.method(request.getMethod())
.path(request.getPath())
.queryParams(request.getQueryParams())
.build();
}
#Value
#Builder
private static class CachedRequest {
RequestPath path;
HttpMethod method;
MultiValueMap<String, String> queryParams;
}
#Value
private static class CachedResponse {
HttpStatus httpStatus;
HttpHeaders headers;
byte[] body;
}
}

Reading response body from ServerHttpResponse Spring cloud gateway

I am trying to read response body from ServerHttpResponse in a FilterFactory class that extents AbstractGatewayFilterFactory. The method executes, but I never see the log line printed. Is this the correct approach to read response ? If yes, what am I missing here ?
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest.Builder reqBuilder = exchange.getRequest().mutate();
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
log.info("Response : {}", new String(content, StandardCharsets.UTF_8));
return bufferFactory.wrap(content);
}));
}
return super.writeWith(body);
}
};
long start = System.currentTimeMillis();
return chain.filter(exchange.mutate()
.request(reqBuilder.build())
.response(decoratedResponse)
.build());
};
}

Spring Cloud gateway send response in filter

I am using spring cloud gateway as edge server.
This is the flow
If request has a header named 'x-foo' then find the header value, get a string from another server and send that string as response instead of actually proxying the request.
Here is code for Filter DSL
#Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("foo-filter", r -> r.header('x-foo').and().header("x-intercepted").negate()
.filters(f -> f.filter(fooFilter))
.uri("http://localhost:8081")) // 8081 is self port, there are other proxy related configurations too
.build();
}
Code for Foo filter
#Component
#Slf4j
public class FooFilter implements GatewayFilter {
#Autowired
private ReactiveRedisOperations<String, String> redisOps;
#Value("${header-name}")
private String headerName;
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
var foo = request.getHeaders().getFirst(headerName);
return redisOps.opsForHash()
.get("foo:" + foo, "response")
.doOnSuccess(s -> {
log.info("data on success");
log.info(s.toString()); // I am getting proper response here
if (s != null) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().set("x-intercepted", "true");
byte[] bytes = s.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
response.writeWith(Mono.just(buffer));
response.setComplete();
}
})
.then(chain.filter(exchange));
}
}
The problem is, the response has the response is getting proper 200 code, the injected header is present on response but the data is not available in response.
This is how I got working.
Use flatMap instead of doOnSuccess
don't use then or switchIfEmpty instead use onErrorResume
Return the response.writeWith
#Component
#Slf4j
public class FooFilter implements GatewayFilter {
#Autowired
private ReactiveRedisOperations<String, String> redisOps;
#Value("${header-name}")
private String headerName;
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
var foo = request.getHeaders().getFirst(headerName);
return redisOps.opsForHash()
.get("foo:" + foo, "response")
.flatMap(s -> {
log.info("data on success");
log.info(s.toString()); // I am getting proper response here
if (s != null) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().set("x-intercepted", "true");
byte[] bytes = s.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}else{ return chain.filter(exchange).then(Mono.fromRunnable(() -> {log.info("It was empty")} }
})
.onErrorResume(chain.filter(exchange));
}
}

Resources