WebClient Request and Response body logging - java

I am trying to make a POJO out of request and response data received when making WebClient calls. But I am not getting the request body in string/JSON readable form instead I am getting a BodyInsertor. I am making use of Exchange Filters.
public ExchangeFilterFunction logWebRequest() {
return (request, next) -> {
log.info("Entered in logWebRequest for WebClient");
long startTime = System.currentTimeMillis();
Mono<ClientResponse> response = next.exchange(request);
long processingTimeInMs = System.currentTimeMillis() - startTime;
// request.body() -> Gives Body Insertor
WebRequestLog webRequestLog = webRequestService.makeWebRequestLog(request, response.block());
webRequestLog.setProcessingTimeInMs(processingTimeInMs);
log.info("WebRequest to be produced to kafka topic: " + webRequestLog);
kafkaService.produceAuditLog(webRequestLog);
return response;
};
}
I followed some articles such as https://andrew-flower.com/blog/webclient-body-logging and https://www.gitmemory.com/issue/spring-projects/spring-framework/24262/570245788 but nothing worked for me.
My end goal is to capture requests and responses with their bodies and produce the data collected for Kafka.

Inside ExchangeFilterFunction, you can access HTTP method, URL, headers, cookies but request or response body can not be accessed directly from this filter.
Refer to the answer here. It provides a way to get access to the request and response body. It also provides a link to This blog post. It explains how to get the body in JSON/String format in Web Client.

You can do tracing of request and response payloads with small manipulations with request and responses:
public class TracingExchangeFilterFunction implements ExchangeFilterFunction {
return next.exchange(buildTraceableRequest(request))
.flatMap(response ->
response.body(BodyExtractors.toDataBuffers())
.next()
.doOnNext(dataBuffer -> traceResponse(response, dataBuffer))
.thenReturn(response)) ;
}
private ClientRequest buildTraceableRequest(
final ClientRequest clientRequest) {
return ClientRequest.from(clientRequest).body(
new BodyInserter<>() {
#Override
public Mono<Void> insert(
final ClientHttpRequest outputMessage,
final Context context) {
return clientRequest.body().insert(
new ClientHttpRequestDecorator(outputMessage) {
#Override
public Mono<Void> writeWith(final Publisher<? extends DataBuffer> body) {
return super.writeWith(
from(body).doOnNext(buffer ->
traceRequest(clientRequest, buffer)));
}
}, context);
}
}).build();
}
private void traceRequest(ClientRequest clientRequest, DataBuffer buffer) {
final ByteBuf byteBuf = NettyDataBufferFactory.toByteBuf(buffer);
final byte[] bytes = ByteBufUtil.getBytes(byteBuf);
// do some tracing e.g. new String(bytes)
}
private void traceResponse(ClientResponse response, DataBuffer dataBuffer) {
final byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
// do some tracing e.g. new String(bytes)
}
}

To add to Vicky Ajmera answer best way to get and log a request is with ExchangeFilterFunction.
private ExchangeFilterFunction logRequest() {
return (clientRequest, next) -> {
logger.info("Request: {} {} {}", clientRequest.method(), clientRequest.url(), clientRequest.body());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return next.exchange(clientRequest);
};
}
But to log a response body you will have to go to the lower level of ClientHttpResponse which then allows you to intercept the body.
First extend ClientHttpResponseDecorator like this:
public class LoggingClientHttpResponse extends ClientHttpResponseDecorator {
private static final Logger logger = LoggerFactory.getLogger(LoggingClientHttpResponse.class);
private static final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
private final DataBuffer buffer = bufferFactory.allocateBuffer();
public LoggingClientHttpResponse(ClientHttpResponse delegate) {
super(delegate);
}
#Override
public Flux<DataBuffer> getBody() {
return super.getBody()
.doOnNext(this.buffer::write)
.doOnComplete(() -> logger.info("Response Body: {}", buffer.toString(StandardCharsets.UTF_8)));
}
}
Then create your implementation of ClientHttpConnector like this:
public class LoggingClientHttpConnector implements ClientHttpConnector {
private final ClientHttpConnector delegate;
public LoggingClientHttpConnector(ClientHttpConnector delegate) {
this.delegate = delegate;
}
#Override
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri, Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
return this.delegate.connect(method, uri, requestCallback).map(LoggingClientHttpResponse::new);
}
}
And last when building your WebClient add a connector:
HttpClient httpClient = HttpClient.create();
ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
WebClient.builder()
.baseUrl("http://localhost:8080")
.clientConnector(new LoggingClientHttpConnectorDecorator(connector))
.filter(logRequest())
.build();

Related

Spring WebClient - how to retry with delay based on response header

A little background
I've been learning Spring Webflux and reactive programming and have gotten stuck on a problem I'm trying to solve around retry logic using Spring Webclient. I've created a client and made successful calls to an external web-service GET endpoint that returns some JSON data.
Problem
When the external service responds with a 503 - Service Unavailable status, the response includes a Retry-After header with a value that indicates how long I should wait before retrying the request. I want to find a way within Spring Webflux/Reactor to tell the webClient to retry it's request after X period, where X is the difference between now and the DateTime that I parse out of the response header.
Simple WebClient GET request
public <T> Mono<T> get(final String url, Class<T> clazz) {
return webClient
.get().uri(url)
.retrieve()
.bodyToMono(clazz);
}
WebClient Builder
I use a builder to create the webClient variable used in the above method, and it's stored as an instance variable in the class.
webClientBuilder = WebClient.builder();
webClientBuilder.codecs(clientCodecConfigurer -> {
clientCodecConfigurer.defaultCodecs();
clientCodecConfigurer.customCodecs().register(new Jackson2JsonDecoder());
clientCodecConfigurer.customCodecs().register(new Jackson2JsonEncoder());
});
webClient = webClientBuilder.build();
Retry When
I've tried to understand and use the retryWhen method with the Retry class, but can't figure out if I can access or pass through the response header value there.
public <T> Mono<T> get(final String url, Class<T> clazz) {
return webClient
.get().uri(url)
.retrieve()
.bodyToMono(clazz);
.retryWhen(new Retry() {
#Override
public Publisher<?> generateCompanion(final Flux<RetrySignal> retrySignals) {
// Can I use retrySignals or retryContext to find the response header somehow?
// If I can find the response header, how to return a "yes-retry" response?
}
})
}
Filter(s) with Extra Logic and DB Interaction
I've also tried to do some extra logic and use filters with the WebClient.Builder, but that only gets me to a point of halting a new request (call to #get) until a previously established Retry-After value has elapsed.
webClientBuilder = WebClient.builder();
webClientBuilder.codecs(clientCodecConfigurer -> {
clientCodecConfigurer.defaultCodecs();
clientCodecConfigurer.customCodecs().register(new Jackson2JsonDecoder());
clientCodecConfigurer.customCodecs().register(new Jackson2JsonEncoder());
});
webClientBuilder.filter(ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
final Clock clock = Clock.systemUTC();
final int id = (int) clientRequest.attribute("id"); // id is saved as an attribute for the request, pull it out here
final long retryAfterEpochMillis = // get epoch millisecond from DB for id
if(epoch is in the past) {
return Mono.just(clientRequest);
} else { // have to wait until epoch passes to send request
return Mono.just(clientRequest).delayElement(Duration.between(clock.instant(), Instant.ofEpochMilli(retryAfterEpochMillis)));
}
})
);
webClient = webClientBuilder.build();
.onStatus(HttpStatus::isError, response -> {
final List<String> retryAfterHeaders = response.headers().header("Retry-After");
if(retryAfterHeaders.size() > 0) {
final long retryAfterEpochMillis = // parse millisecond epoch time from header
// Save millisecond time to DB associated to specific id
}
return response.bodyToMono(String.class).flatMap(body ->
Mono.error(new RuntimeException(
String.format("Request url {%s} failed with status {%s} and reason {%s}",
url,
response.rawStatusCode(),
body))));
})
Any help is appreciated, and if I can provide more contextual data to help, I will.
1. Retrieve header in retry builder
public class WebClientStatefulRetry3 {
public static void main(String[] args) {
WebClient webClient = WebClient.create();
call(webClient)
.retryWhen(Retry.indefinitely()
.filter(ex -> ex instanceof WebClientResponseException.ServiceUnavailable)
.doBeforeRetryAsync(signal -> Mono.delay(calculateDelay(signal.failure())).then()))
.block();
}
private static Mono<String> call(WebClient webClient) {
return webClient.get()
.uri("http://mockbin.org/bin/b2a26614-0219-4018-9446-c03bc1868ebf")
.retrieve()
.bodyToMono(String.class);
}
private static Duration calculateDelay(Throwable failure) {
String headerValue = ((WebClientResponseException.ServiceUnavailable) failure).getHeaders().get("Retry-After").get(0);
return // calculate delay here from header and current time;
}
}
2. Use expand operator to access the previous response and generate the next one
public class WebClientRetryWithExpand {
public static void main(String[] args) {
WebClient webClient = WebClient.create();
call(webClient)
.expand(prevResponse -> {
List<String> header = prevResponse.headers.header("Retry-After");
if (header.isEmpty()) {
return Mono.empty();
}
long delayInMillis = // calculate delay from header and current time
return Mono.delay(Duration.ofMillis(delayInMillis))
.then(call(webClient));
})
.last()
.block();
}
private static Mono<ResponseWithHeaders> call(WebClient webClient) {
return webClient.get()
.uri("https://example.com")
.exchangeToMono(response -> response.bodyToMono(String.class)
.map(rawResponse -> new ResponseWithHeaders(rawResponse, response.headers())));
}
#Data
static class ResponseWithHeaders {
private final String rawResponse;
private final ClientResponse.Headers headers;
}
}

How to implement custom error handling with retroift2

I calling to the api with the basic retrofit Call object:
public interface dataApi {
#GET("animal/cats")
Call<AllAnimals> getAllData(
#Query("api_key") String apiKey
);
}
And I can get the response inside my view model like this:
call.enqueue(new Callback<AllAnimals>() {
#Override
public void onResponse(Call<AllAnimals> call, Response<AllAnimals> response) {
animals.setValue(response.body());
}
#Override
public void onFailure(Call<AllAnimals> call, Throwable t) {
Log.i(TAG, "onFailure: " + t);
}
});
Nothing speical here.
I've several problem with this approach
FIRST - if I give the wrong api key for example, the response should give me a response with the code of the problem, instead I just get null body.
SECOND I am planning to have more api calls, and it's a huge code duplication to handle errors every call I wrote.
How can I implement custom error handling for this situation, that will be apply to other calls too?
I think you can use okhttp interceptor and define yourself ResponseBody converter to fix your problem.
First,intercept you interested request and response;
Second,check the response,if response is failed then modify the response body to empty。
define a simple interceptor
Interceptor interceptor = new Interceptor() {
#Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String url = request.url().toString();
System.out.println(request.url());
okhttp3.Response response = chain.proceed(request);
if (!response.isSuccessful() && url.contains("animal/cats")) {
// request failed begin to modify response body
response = response.newBuilder()
.body(ResponseBody.create(MediaType.parse("application/json"), new byte[] {}))
.build();
}
return response;
}
};
define self ResponseBody converter
most code from com.squareup.retrofit2:converter-jackson we just add two lines:
final class JacksonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final ObjectReader adapter;
JacksonResponseBodyConverter(ObjectReader adapter) {
this.adapter = adapter;
}
#Override public T convert(ResponseBody value) throws IOException {
try {
if (value.contentLength() == 0) {
return null;
}
return adapter.readValue(value.charStream());
} finally {
value.close();
}
}
}
the below code is added:
if (value.contentLength() == 0) {
return null;
}

Feign Client Error Handling - Suppress the Error/Exception and convert to 200 success response

I am using feign client to connect to downstream service.
I got a requirement that when one of the downstream service endpoint returns 400 ( it's partial success scenario ) our service need this to be converted to 200 success with the response value.
I am looking for a best way of doing this.
We are using error decoder to handle the errors and the above conversion is applicable for only one endpoint not for all the downstream endpoints and noticed that decode() method should returns exception back.
You will need to create a customized Client to intercept the Response early enough to change the response status and not invoke the ErrorDecoder. The simplest approach is to create a wrapper on an existing client and create a new Response with a 200 status. Here is an example when using Feign's ApacheHttpClient:
public class ClientWrapper extends ApacheHttpClient {
private ApacheHttpClient delegate;
public ClientWrapper(ApacheHttpClient client) {
this.client = client;
}
#Override
public Response execute(Request request, Request.Options options) throws IOException {
/* execute the request on the delegate */
Response response = this.client.execute(request, options);
/* check the response code and change */
if (response.status() == 400) {
response = Response.builder(response).status(200).build();
}
return response;
}
}
This customized client can be used on any Feign client you need.
Another way of doing is by throwing custom exception at error decoder and convert this custom exception to success at spring global exception handler (using #RestControllerAdvice )
public class CustomErrorDecoder implements ErrorDecoder {
#Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 400 && response.request().url().contains("/wanttocovert400to200/clientendpoints") {
ResponseData responseData;
ObjectMapper mapper = new ObjectMapper();
try {
responseData = mapper.readValue(response.body().asInputStream(), ResponseData.class);
} catch (Exception e) {
responseData = new ResponseData();
}
return new PartialSuccessException(responseData);
}
return FeignException.errorStatus(methodKey, response);
}}
And the Exception handler as below
#RestControllerAdvice
public class GlobalControllerExceptionHandler {
#ResponseStatus(HttpStatus.OK)
#ExceptionHandler(PartialSuccessException.class)
public ResponseData handlePartialSuccessException(
PartialSuccessException ex) {
return ex.getResponseData();
}
}
Change the microservice response:
public class CustomFeignClient extends Client.Default {
public CustomFeignClient(
final SSLSocketFactory sslContextFactory, final HostnameVerifier
hostnameVerifier) {
super(sslContextFactory, hostnameVerifier);
}
#Override
public Response execute(final Request request, final Request.Options
options) throws IOException {
Response response = super.execute(request, options);
if (HttpStatus.SC_OK != response.status()) {
response =
Response.builder()
.status(HttpStatus.SC_OK)
.body(InputStream.nullInputStream(), 0)
.headers(response.headers())
.request(response.request())
.build();
}
return response;
}
}
Add a Feign Client Config:
#Configuration
public class FeignClientConfig {
#Bean
public Client client() {
return new CustomFeignClient(null, null);
}
}

Spring boot ClientHttpRequestInterceptor resend on 401

So i have below scenario to implement using Spring boot rest template to consume a REST-API (involves token authentication mechanism). To perform test i've created simple mock REST API in spring boot. Here's the process,
From my API consumer app,
sends a request using rest-template to consume a protected API, this API requires Authorization: Bearer <token> header to be present in request.
if something is wrong with this token (missing header, invalid token), protected API returns HTTP-Unauthorized (401).
when this happens, consumer API should send another request to another protected API that returns a valid access token, this protected API requires Authorization: Basic <token> header to be present. New access token will be stored in a static field and it will be used in all other requests to authenticate.
This can be achieved by simply catching 401-HttpClientErrorException in RestTemplate consumer methods (postForObject), but the idea was to decouple it from REST-API consumer classes. To achieve it, i tried to use ClientHttpRequestInterceptor
Here's the code, that i tried so far.
Interceptor class
public class AuthRequestInterceptor implements ClientHttpRequestInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthRequestInterceptor.class);
private static final String BASIC_AUTH_HEADER_PREFIX = "Basic ";
private static final String BEARER_AUTH_HEADER_PREFIX = "Bearer ";
//stores access token
private static String accessToken = null;
#Value("${app.mife.apiKey}")
private String apiKey;
#Autowired
private GenericResourceIntegration resourceIntegration; // contains methods of rest template
#Override
public ClientHttpResponse intercept(
HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution
) throws IOException {
LOGGER.info("ReqOn|URI:[{}]{}, Headers|{}, Body|{}", request.getMethod(), request.getURI(), request.getHeaders(), new String(body));
request.getHeaders().add(ACCEPT, APPLICATION_JSON_VALUE);
request.getHeaders().add(CONTENT_TYPE, APPLICATION_JSON_VALUE);
try {
//URI is a token generate URI, request
if (isBasicUri(request)) {
request.getHeaders().remove(AUTHORIZATION);
//sets BASIC auth header
request.getHeaders().add(AUTHORIZATION, (BASIC_AUTH_HEADER_PREFIX + apiKey));
ClientHttpResponse res = execution.execute(request, body);
LOGGER.info("ClientResponse:[{}], status|{}", "BASIC", res.getStatusCode());
return res;
}
//BEARER URI, protected API access
ClientHttpResponse response = null;
request.getHeaders().add(AUTHORIZATION, BEARER_AUTH_HEADER_PREFIX + getAccessToken());
response = execution.execute(request, body);
LOGGER.info("ClientResponse:[{}], status|{}", "BEARER", response.getStatusCode());
if (unauthorized(response)) {
LOGGER.info("GetToken Res|{}", response.getStatusCode());
String newAccessToken = generateNewAccessCode();
request.getHeaders().remove(AUTHORIZATION);
request.getHeaders().add(AUTHORIZATION, (BEARER_AUTH_HEADER_PREFIX + newAccessToken));
LOGGER.info("NewToken|{}", newAccessToken);
return execution.execute(request, body);
}
if (isClientError(response) || isServerError(response)) {
LOGGER.error("Error[Client]|statusCode|{}, body|{}", response.getStatusCode(), CommonUtills.streamToString(response.getBody()));
throw new AccessException(response.getStatusText(),
ServiceMessage.error().code(90).payload(response.getRawStatusCode() + ":" + response.getStatusText()).build());
}
return response;
} catch (IOException exception) {
LOGGER.error("AccessError", exception);
throw new AccessException("Internal service call error",
ServiceMessage.error().code(90).payload("Internal service call error", exception.getMessage()).build()
);
} finally {
LOGGER.info("ReqCompletedOn|{}", request.getURI());
}
}
private String generateNewAccessCode() {
Optional<String> accessToken = resourceIntegration.getAccessToken();
setAccessToken(accessToken.get());
return getAccessToken();
}
private static void setAccessToken(String token) {
accessToken = token;
}
private static String getAccessToken() {
return accessToken;
}
private boolean isClientError(ClientHttpResponse response) throws IOException {
return (response.getRawStatusCode() / 100 == 4);
}
private boolean isServerError(ClientHttpResponse response) throws IOException {
return (response.getRawStatusCode() / 100 == 5);
}
private boolean unauthorized(ClientHttpResponse response) throws IOException {
return (response.getStatusCode().value() == HttpStatus.UNAUTHORIZED.value());
}
private boolean isBasicUri(HttpRequest request) {
return Objects.equals(request.getURI().getRawPath(), "/apicall/token");
}
private boolean isMifeRequest(HttpRequest request) {
return request.getURI().toString().startsWith("https://api.examplexx.com/");
}
}
Token generate method- In resourceIntegration
public Optional<String> getAccessToken() {
ResponseEntity<AccessTokenResponse> res = getRestTemplate().exchange(
getAccessTokenGenUrl(),
HttpMethod.POST,
null,
AccessTokenResponse.class
);
if (res.hasBody()) {
LOGGER.info(res.getBody().toString());
return Optional.of(res.getBody().getAccess_token());
} else {
return Optional.empty();
}
}
Another sample protected API call method
public Optional<String> getMobileNumberState(String msisdn) {
try {
String jsonString = getRestTemplate().getForObject(
getQueryMobileSimImeiDetailsUrl(),
String.class,
msisdn
);
ObjectNode node = new ObjectMapper().readValue(jsonString, ObjectNode.class);
if (node.has("PRE_POST")) {
return Optional.of(node.get("PRE_POST").asText());
}
LOGGER.debug(jsonString);
} catch (IOException ex) {
java.util.logging.Logger.getLogger(RestApiConsumerService.class.getName()).log(Level.SEVERE, null, ex);
}
return Optional.empty();
}
Problem
Here's the log of mock API,
//first time no Bearer token, this returns 401 for API /simulate/unauthorized
accept:text/plain, application/json, application/*+json, */*
authorization:Bearer null
/simulate/unauthorized
//then it sends Basic request to get a token, this is the log
accept:application/json, application/*+json
authorization:Basic M3ZLYmZQbE1ERGhJZWRHVFNiTEd2Vlh3RThnYTp4NjJIa0QzakZUcmFkRkVOSEhpWHNkTFhsZllh
Generated Token:: 57f21374-1188-4c59-b5a7-370eac0a0aed
/apicall/token
//finally consumer API sends the previous request to access protected API and it contains newly generated token in bearer header
accept:text/plain, application/json, application/*+json, */*
authorization:Bearer 57f21374-1188-4c59-b5a7-370eac0a0aed
/simulate/unauthorized
The problem is even-though mock API log had the correct flow, consumer API does not get any response for third call, here's the log of it (unnecessary logs are omitted).
RequestInterceptor.intercept() - ReqOn|URI:[GET]http://localhost:8080/simulate/unauthorized?x=GlobGlob, Headers|{Accept=[text/plain, application/json, application/*+json, */*], Content-Length=[0]}, Body|
RequestInterceptor.intercept() - ClientResponse:[BEARER], status|401 UNAUTHORIZED
RequestInterceptor.intercept() - GetToken Res|401 UNAUTHORIZED
RequestInterceptor.intercept() - ReqOn|URI:[POST]http://localhost:8080/apicall/token?grant_type=client_credentials, Headers|{Accept=[application/json, application/*+json], Content-Length=[0]}, Body|
RequestInterceptor.intercept() - ClientResponse:[BASIC], status|200 OK
RequestInterceptor.intercept() - ReqCompletedOn|http://localhost:8080/apicall/token?grant_type=client_credentials
RestApiConsumerService.getAccessToken() - |access_token2163b0d4-8d00-4eba-92d0-7e0bb609b982,scopeam_application_scope default,token_typeBearer,expires_in34234|
RequestInterceptor.intercept() - NewToken|2163b0d4-8d00-4eba-92d0-7e0bb609b982
RequestInterceptor.intercept() - ReqCompletedOn|http://localhost:8080/simulate/unauthorized?x=GlobGlob
http://localhost:8080/simulate/unauthorized third time does not return any response, but mock API log says it hit the request. What did i do wrong ?, is it possible to achieve this task using this techniques ? or is there any other alternative way to do this ? any help is highly appreciated.
I have tried this:
Add an interceptor ClientHttpRequestInterceptor
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StringUtils;
public class RequestResponseHandlerInterceptor implements ClientHttpRequestInterceptor {
#Autowired
private TokenService tokenService;
#Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String AUTHORIZATION = "Authorization";
/**
* This method will intercept every request and response and based on response status code if its 401 then will retry
* once
*/
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
ClientHttpResponse response = execution.execute(request, body);
if (HttpStatus.UNAUTHORIZED == response.getStatusCode()) {
String accessToken = tokenService.getAccessToken();
if (!StringUtils.isEmpty(accessToken)) {
request.getHeaders().remove(AUTHORIZATION);
request.getHeaders().add(AUTHORIZATION, accessToken);
//retry
response = execution.execute(request, body);
}
}
return response;
}
}
Apart from this you need to override RestTemplate initialization as well.
#Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new RequestResponseHandlerInterceptor()));
return restTemplate;
}

Spring Cloud Gateway: Modified Response Body is Truncated

I've been experimenting with Spring Cloud Gateway a bit and I'm trying to modify the response body. Using a response decorator, I'm able to see that the body is modified, however, the buffer size is still the size of the original response. Is there a way to expand the buffer size to the size of the new response body?
public class ModifyBodyGatewayFilterImpl implements GatewayFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("\n\nexchange.getAttributes():\n {}\n\n", exchange.getAttributes());
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory dataBufferFactory = response.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
Flux<? extends DataBuffer> f = flux.flatMap( dataBuffer -> {
byte[] origRespContent = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(origRespContent);
System.out.println("content::: " + (new String(origRespContent)));
//alocating a new buffer size does not help.
DataBuffer b = dataBufferFactory.allocateBuffer(256);
b.write("0123456789abcdefg".getBytes());
return Flux.just(b);
});
return super.writeWith(f);
}
};
ServerWebExchange swe = exchange.mutate().response(decoratedResponse).build();
return chain.filter(swe);
}
}
Example: Expected re-written response is 0123456789abcdefg If original content is 11 bytes <p>test</p>, then the re-written response is truncated to 0123456789a.
I resolved this issue using buffer() method:
public class ModifyBodyGatewayFilterImpl implements GatewayFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory dataBufferFactory = response.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
return super.writeWith(flux.buffer().map(dataBuffers -> {
ByteOutputStream outputStream = new ByteOutputStream();
dataBuffers.forEach(i -> {
byte[] array = new byte[i.readableByteCount()];
i.read(array);
outputStream.write(array);
});
outputStream.write("0123456789abcdefg".getBytes());
return dataBufferFactory.wrap(outputStream.getBytes());
}));
}
return super.writeWith(body);
}
};
ServerWebExchange swe = exchange.mutate().response(decoratedResponse).build();
return chain.filter(swe);
}
}
it should put all response chunks in buffer which should be released when flux is completed.
you can use this
// prepare the mono to be returned
DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
ObjectMapper objMapper = new ObjectMapper();
byte[] obj;
try {
obj = objMapper.writeValueAsBytes(response);
return exchange.getResponse().writeWith(Mono.just(obj).map(r -> dataBufferFactory.wrap(r)));
} catch (JsonProcessingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return exchange.getResponse().setComplete();
where the response is any object you want it must be serializable.
You need to rewrite the content-length header also.
Like this:
byte[] bytes = "0123456789abcdefg".getBytes();
DataBuffer b = dataBufferFactory.wrap(bytes);
response.getHeaders().setContentLength(bytes.length);
I hope this helps :)
I was experiencing the same problem, data was received in two buffers. Based on an #Alex solution I've improved it by joining but without ByteOutpoutStream.
In my solution I used DefaultDataBufferFactory and join() method.
#Override
public Mono<Void> writeWith(final Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
var joinedBuffers = new DefaultDataBufferFactory().join(dataBuffers);
byte[] content = new byte[joinedBuffers.readableByteCount()];
joinedBuffers.read(content);
final var responseBody = new String(content, StandardCharsets.UTF_8);
// modify body
return bufferFactory.wrap(responseBody.getBytes());
}));
}
return super.writeWith(body);
}
I was looking for a general way to modify response (I'm basically modifying the JSON body) without having the need to deal with Flux (i.e., Mono is fine) so i started to look at what SCG's ModifyResponseBodyGatewayFilterFactory does today and with the help of the information from this gist and from this blog entry.
I was able to come up with a quick-and-dirty solution and wrote it up here. Basically I just modified what ModifyResponseBodyGatewayFilterFactory is doing today and change it a little bit for my use case. A fragment of the solution is here:
#SuppressWarnings("unchecked")
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
Class inClass = String.class;
Class outClass = String.class;
String originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType);
ClientResponse clientResponse = prepareClientResponse(body, httpHeaders);
// TODO: flux or mono
Mono modifiedBody = extractBody(exchange, clientResponse, inClass)
.flatMap(originalBody -> Mono.just(applyTransform((String) originalBody, config))
.switchIfEmpty(Mono.empty());
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange,
exchange.getResponse().getHeaders());
return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
Mono<DataBuffer> messageBody = writeBody(getDelegate(), outputMessage, outClass);
HttpHeaders headers = getDelegate().getHeaders();
if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)
|| headers.containsKey(HttpHeaders.CONTENT_LENGTH)) {
messageBody = messageBody.doOnNext(data -> headers.setContentLength(data.readableByteCount()));
}
// TODO: fail if isStreamingMediaType?
return getDelegate().writeWith(messageBody);
}));
}
Again, this solution may not be what is needed here but hopefully this will help someone who needs a similar solution.

Categories