Spring WebClient how to log each retry at INFO level? - spring

I have following WebClient - makes http call to localhost:8090 - bean defined:
#Configuration
class WebClientConfig {
#Bean
public WebClient webClientBean() {
return WebClient.create("http://localhost:8090");
}
}
And a call another service (8090):
Response response = requestBodySpec
.retrieve()
.bodyToMono(Response.class)
.timeout(Duration.ofSeconds(5))
.retry(2L)
.doOnSuccess(res -> log.info(res))
.doOnError(err -> log.error(err))
.block();
I see the error after timing out:
java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 5000ms in 'flatMap'
But I am not seeing logs for the 2 additional retries that I specified; they are only visible when I turn on TRACE:
TRACE 2120 --- [nio-8080-exec-1] o.s.w.r.f.client.ExchangeFunctions : [9c7e1e0] HTTP POST http://localhost:8090/test, headers={masked}
Is there any way I can log the event whenever there is a retry happening? Similar to above log except I have to INFO log it.

You can try specifying a RetrySpec instead, which gives more control over the retry behavior and provides a "callback" for doing non-blocking things.
Assuming your logger is non-blocking you can do something like this:
RetryBackOffSpec retrySpec = Retry.fixedDelay(2L, Duration.ofMillis(25))
.doAfterRetry(signal -> log.info("Retry number {}", signal.totalRetries()));
Response response = requestBodySpec
.retrieve()
.bodyToMono(Response.class)
.timeout(Duration.ofSeconds(5))
.retry(retrySpec)
.doOnSuccess(res -> log.info(res))
.doOnError(err -> log.error(err))
.block();

Related

Spring WebClient ClientResponse times out on bodyToMono

This is the API call (blocking) I am making to external application concurrently (max of 100 calls at the same time)
ClientResponse response = webclient.get()
.uri("https-url-here-with-query-params-here")
.header(HttpHeaders.AUTHORIZATION, "abcdefg")
.exchange()
.timeout(Duration.ofSeconds(300))
.block();
if (response.statusCode().is2xxSuccessful()) {
MyPojoEntity myPojo = response.bodyToMono(MyPojoEntity.class)
.timeout(Duration.ofSeconds(300))
.block();
}
What I observe is the call to response.bodyToMono(MyPojoEntity.class).timeout(Duration.ofSeconds(300)) is timing out after 5 minutes. My understanding is ClientResponse already has response body from the server and response.bodyToMono method just unmarshalling to pojo entity class. The payload is very small and shouldn't take more than few seconds to unmarshall it. May be it is still reading the response from server and timing out due to API issue on the server? If that is the case, then what does if (response.statusCode().is2xxSuccessful()) mean? I expect when response status is success, payload also to be there especially when i retrieve ClientResponse in a blocking way. Please help me to understand what is going on here.

suppress stacktrace from spring webclient/netty layer

In one of the apps my team manages, there's a GraphQL orchestration layer that calls a downstream service.
We use Spring's webclient for it.
WebClient Config.
WebClient.Builder webClientBuilder(MetricsWebClientCustomizer metricsCustomizer) {
final HttpClient httpClient = HttpClient.create(ConnectionProvider.fixed("webClientPool", maxConnections))
.tcpConfiguration(client ->
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeoutMillis)
.option(EpollChannelOption.TCP_KEEPIDLE, tcpKeepIdleInSec)
.option(EpollChannelOption.TCP_KEEPINTVL, tcpKeepIntvlInSec)
.option(EpollChannelOption.TCP_KEEPCNT, tcpKeepCount)
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(readTimeoutInSec))
));
final WebClient.Builder webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient));
metricsCustomizer.customize(webClient);
return webClient;
}
return client.post()
.uri(uriBuilder -> buildUri())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromObject(request))
.retrieve()
.bodyToMono(Result.class)
.compose(CircuitBreakerOperator.of(cb))
.block(Duration.ofSeconds(blockDuration));
This setup works well. However, I see a lot of
io.netty.handler.timeout.ReadTimeoutException: null
Suppressed: java.lang.Exception: #block terminated with an error
at reactor.core.publisher. .blockingGet(BlockingSingleSubscriber.java:133)
at reactor.core.publisher.Mono.block(Mono.java:1518)
at com.xxxx.c.g.xx.client.Client.get(Client.java:293)
at com.xxxx.c.g.xx.resolver.impl.xxQueryImpl.lambda$xx$171(xxQueryImpl.java:2187)
at io.micrometer.core.instrument.AbstractTimer.record(AbstractTimer.java:149)
Every timeout results in this chunky stacktrace. It's the same message that gets repeated. Does give any useful info. Is there a way to get WebClient/Netty print this once and ignore the rest?
BlockingSingleSubscriber.blockingGet seems to be doing this. Keeps track of previous exceptions.
Throwable e = this.error;
if (e != null) {
RuntimeException re = Exceptions.propagate(e);
re.addSuppressed(new Exception("#block terminated with an error"));
throw re;
} else {
return this.value;
}
I tried adding error handlers to the calling function bodyToMono(Result.class).doOnError("log").block();
This results in the line "log" from the doOnError consumer getting printed along with the chunky stacktraces.
Any ideas?
Full stacktrace:
https://pastebin.com/kdpspCEY
Not sure if you've solved the issue yet, but I had the same problem. For anyone with a similar issue, I solved it by setting the following in the application.properties file:
# Logging Information
logging.level.reactor.netty=off
This disabled Reactor Netty internal logging, which was a bit verbose for what I needed.

Async RabbitMQ communcation using Spring Integration

I have two spring boot services that communicate using RabbitMQ.
Service1 sends request for session creation to Service2.
Service2 handles request and should return response.
Service1 should handle the response.
Service1 method for requesting session:
public void startSession()
{
ListenableFuture<SessionCreationResponseDTO> sessionCreationResponse = sessionGateway.requestNewSession();
sessionCreationResponse.addCallback(response -> {
//handle success
}, ex -> {
// handle exception
});
}
On Service1 I have defined AsyncOutboundGateway, like:
#Bean
public IntegrationFlow requestSessionFlow(MessageChannel requestNewSessionChannel,
AsyncRabbitTemplate amqpTemplate,
SessionProperties sessionProperties)
{
return flow -> flow.channel(requestNewSessionChannel)
.handle(Amqp.asyncOutboundGateway(amqpTemplate)
.exchangeName(sessionProperties.getRequestSession().getExchangeName())
.routingKey(sessionProperties.getRequestSession().getRoutingKey()));
}
On Service2, I have flow for receiving these messages:
#Bean
public IntegrationFlow requestNewSessionFlow(ConnectionFactory connectionFactory,
SessionProperties sessionProperties,
MessageConverter messageConverter,
RequestNewSessionHandler requestNewSessionHandler)
{
return IntegrationFlows.from(Amqp.inboundGateway(connectionFactory,
sessionProperties.requestSessionProperties().queueName())
.handle(requestNewSessionHandler)
.get();
Service2 handles there requests:
#ServiceActivator(async = "true")
public ListenableFuture<SessionCreationResponseDTO> handleRequestNewSession()
{
SettableListenableFuture<SessionCreationResponseDTO> settableListenableFuture = new SettableListenableFuture<>();
// Goes through asynchronous process of creating session and sets value in listenable future
return settableListenableFuture;
}
Problem is that Service2 immediately returns ListenableFuture to Service1 as message payload, instead of waiting for result of future and sending back result.
If I understood documentation correctly Docs by setting async parameter in #ServiceActivator to true, successful result should be returned and in case of exception, error channel would be used.
Probably I misunderstood documentation, so that I need to unpack ListenableFuture in flow of Service2 before returning it as response, but I am not sure how to achieve that.
I tried something with publishSubscribeChannel but without much luck.
Your problem is here:
.handle(requestNewSessionHandler)
Such a configuration doesn't see your #ServiceActivator(async = "true") and uses it as a regular blocking service-activator.
Let's see if this helps you:
.handle(requestNewSessionHandler, "handleRequestNewSession", e -> e.async(true))
It is better to think about it like: or only annotation configuration. or only programmatic, via Java DSL.

Webclient Error Handling - Spring Webflux

I want to throw my custom exceptions with the following conditions:
If I am getting proper error response in json format, I want to deserialize it and throw my exception named CommonException inside onStatus()
If I am receiving an HTML content as part of response or deserialization didnt happen successfully then I want to throw GenericException which I am creating inside onErrorMap()
While throwing a GenericException, I want to pass the same HttpStatus code to upstream which I am getting from downstream response.
IdVerificationResponse idVerificationResponse = client.get()
.uri(idVerificationUrl)
.headers(headers -> headers.addAll(httpEntity.getHeaders()))
.retrieve()
.onStatus(HttpStatus::isError, response ->
//Throw this one only if deserialization of error response to IdVerificationErrorResponse class happens successfully
response.bodyToMono(IdVerificationErrorResponse.class)
.flatMap(error -> Mono.error(CommonException.builder().message(error.getCustomMessage()).build()))
)
.bodyToMono(IdVerificationResponse.class)
.onErrorMap(error -> {
//Come over here only if there is an error in deserialization in onStatus()
//How to get the HttpStatus we are getting as part of error response from the downstream
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
ApiErrorDetails errorDetailsObj = ApiErrorDetails.builder().errorCode(httpStatus.name()).errorDescription("Error related to HTML")
.errorDetails("Error related to HTML").build();
ErrorDetails errorDetails = ErrorDetails.builder().errors(errorDetailsObj).build();
return GenericException.builder().errorDetails(errorDetails).httpStatus(httpStatus).build();
}).block();
Currently onErrorMap() is getting called everytime and overriding the exception I am throwing inside onStatus()
Found the solution
IdVerificationResponse idVerificationResponse = client.get()
.uri(processCheckUrl)
.headers(headers -> headers.addAll(httpEntity.getHeaders()))
.retrieve()
.onStatus(HttpStatus::isError, response -> {
HttpStatus errorCode = response.statusCode();
return response.bodyToMono(IdVerificationErrorResponse.class)
.onErrorMap(error -> new Exception("Throw your generic exception over here if there is any error in deserialization"))
.flatMap(error -> Mono.error(new Exception("Throw your custom exception over here after successful deserialization")));
})
.bodyToMono(IdVerificationResponse.class).block();

Add Exception handler for Spring Web Client

I use this code for REST API requests.
WebClient.Builder builder = WebClient.builder().baseUrl(gatewayUrl);
ClientHttpConnector httpConnector = new ReactorClientHttpConnector(opt -> opt.sslContext(sslContext));
builder.clientConnector(httpConnector);
How I can add connection exception handler? I would like to implement some custom logic? Is this feature easy to implement?
If I understand your question in the context of failing the connection because of SSL credentials, then you should see the connection exception manifest itself on the REST response.
You can take care of that exception via the Flux result you get on WebClient.ResponseSpec#onStatus. The docs for #onStatus says:
Register a custom error function that gets invoked when the given
HttpStatus predicate applies. The exception returned from the function
will be returned from bodyToMono(Class) and bodyToFlux(Class). By
default, an error handler is register that throws a
WebClientResponseException when the response status code is 4xx or
5xx.
Take a look at this example:
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxServerError, response -> ...) // This is in the docs there but is wrong/fatfingered, should be is4xxClientError
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
Similarly for your question, the connection error should manifest itself after the call gets made and you can customize how it gets propogated in the reactive pipeline:
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> {
... Code that looks at the response more closely...
return Mono.error(new MyCustomConnectionException());
})
.bodyToMono(Person.class);
Hope that helps.

Resources