Spring WebClient - how to log sucessfull and failed response? - java

I'm trying to create a REST call using WebClient
statusWebClient.post()
.uri(url)
.bodyValue(createBody(state, number))
.retrieve()
.bodyToFlux(String.class)
.doOnEach(response -> log.debug("Notification was sent to {}, response {}", url, response.get()))
.doOnError(exception -> log.warn("Failed to send notification to {}, cause {}", url, exception.getMessage()))
.subscribe();
I only want to log the call result. On success - log successful message with response body, on 5XX or timeout or other - log the error message. The log should be created in the background (not by the thread that created the call)
But doOnEach is executed each time, doOnError works fine, but there is also
reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.web.reactive.function.client.WebClientResponseException$InternalServerError: 500 Internal Server Error from POST
logged in the logfile.
I've I also seen in few tutorials onSuccess method, but in my setup there is such method.
How can I log success and fail messages?

This is what I created. Seems that works fine
statusWebClient.post()
.uri(url)
.bodyValue(createTerminalStatusBody(state, msisdn))
.retrieve()
.bodyToMono(String.class)
.subscribeOn(Schedulers.boundedElastic())
.doOnSuccess(response -> log.debug("Notification was sent to {}, response {}", url, response))
.doOnError(exception -> log.warn("Failed to send notification to {}, cause {}", url, exception.getMessage()))
.onErrorResume(throwable -> Mono.empty())
.subscribe();

You could setup custom filters for the WebClient
WebClient.builder()
.filter(logRequest())
.filter(logResponse());
private ExchangeFilterFunction logRequest() {
return (clientRequest, next) -> {
log.debug("Request: {} {}", clientRequest.method(), clientRequest.url());
return next.exchange(clientRequest);
};
}
private ExchangeFilterFunction logResponse() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
log.debug("Response Status: {}", clientResponse.statusCode());
return Mono.just(clientResponse);
});
}

Related

Retry only when 5xx error is encountered Spring boot WebClient

I have followed few suggestions around to make a retry attempt only when I get 5xx error and straight away throw exception otherwise. But the code example I got doesn't seems to be working. It's not retrying for any of the exceptions (be it 4xx or 5xx).
Here is my code
public Mono<String> call(String input) {
log.info("Sending echo call for: {}", input);
return webClient.get()
.uri(format("/echo/%s", input))
.accept(MediaType.APPLICATION_JSON).acceptCharset(StandardCharsets.UTF_8)
.retrieve()
.onStatus(HttpStatus::is5xxServerError,
response -> Mono.error(new ServiceException("Server error", response.rawStatusCode())))
.bodyToMono(String.class)
.timeout(ofMillis(60_000L))
.retryWhen(
backoff(3L, ofSeconds(4L))
.jitter(0d)
.filter(ServiceException.class::isInstance)
.doAfterRetry(retrySignal -> log.info("Retry call: " + retrySignal))
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> retrySignal.failure())
)
.doOnError(throwable -> log.error("Call failure: {}", throwable.getMessage()))
.doOnSuccess(response -> log.info("Success: {}", response));
}
Anyone can help me figure out what's wrong with the code or can suggest an alternative approach. Thanks a lot!
Please try this, it should work.
final WebClient webClient = WebClient.builder().build();
final Mono<String> body = webClient
.get()
.uri("URL")
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry
.backoff(3, Duration.ofSeconds(2))
.filter(this::is5xxServerError)
);
System.out.println(body.block());
private boolean is5xxServerError(Throwable throwable) {
return throwable instanceof WebClientResponseException &&
((WebClientResponseException) throwable).getStatusCode().is5xxServerError();
}

Spring WebClient exchangeToMono seem to return nothing

I am consuming an external API asynchronously using Spring WebClient and use exchangeToMono(). Here is my code block:
`Mono<ExternalServiceResponse> externalServiceResponseMono = webClient.get()
.header("trackingid", "abc123")
.exchangeToMono(clientResponse -> {
if(clientResponse.statusCode().equals(HttpStatus.OK)) {
return clientResponse.bodyToMono(ExternalServiceResponse.class);
}
else if (clientResponse.statusCode()
.is4xxClientError()) {
log.error("ErrorResponse Code from ExternalService API is: " + clientResponse.rawStatusCode());
return Mono.error(new RestServiceCallException(clientResponse.statusCode().getReasonPhrase()));
}else if (clientResponse.statusCode()
.is5xxServerError()) {
return Mono.error(new ExternalServiceCallException(clientResponse.statusCode().getReasonPhrase()));
}
return Mono.error(new RuntimeException("Unknown error occured while calling External Service API"));
})
.retryWhen(Retry.fixedDelay(2, Duration.ofSeconds(2)).filter(throwable -> throwable instanceof ExternalServiceCallException ));`
Then I subscribe to Mono response as below:
List<Response> processedResponseList = new ArrayList<>();
externalServiceResponseMono.subscribe(res -> res.getDetails().forEach(section -> processedResponseList.add(processResponseExternalAPI(section))));
I am not seeing anything being subscribed from externalServiceResponseMono (even though API returns data, verified in postman). The processedResponseList is always empty.
What am I missing? Why do I see no data being subscribed from Mono?

"IllegalStateException: Only one connection receive subscriber allowed" when response body is empty

I have a Spring Boot 2.3.1 project, in which I use WebClient to call a remote service.
The remote service is not very reliable and tends to return 500 errors, with and without response bodies. My goal is throw a custom exception that contains the response body (or a default message) so that I can log it, and here's my code :
webClient.get()
.uri(targetServiceUri)
.retrieve()
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(error ->
Mono.error(new MyCustomServiceException(error))
)
)
.toEntity(String.class)
.block();
I have 2 tests using wiremock, the first one works :
#Test
void shouldThrowCustomExceptionWhenServiceReturns500ServerErrorWithNoBody() {
setStubForInValidCheckCall(HttpStatus.INTERNAL_SERVER_ERROR,"{'Error':'invalid request'}");
Throwable thrown =
catchThrowable(() -> myClient.performComplianceCheck(getCompany()));
assertThat(thrown)
.isInstanceOf(MyCustomServiceException.class)
.hasMessageContaining("{'Error':'invalid request'}");
}
private void setStubForInValidCheckCall(HttpStatus httpStatus, String body) {
var response= aResponse().withStatus(httpStatus.value());
if(body!=null){
response=response.withBody(body);
}
stubFor(
get(urlPathMatching("/targetCompliance"))
.willReturn(response));
}
However, the second test in which the response is 500 but there's no body (or if it's an empty string), fails with "java.lang.IllegalStateException: Only one connection receive subscriber allowed.
#Test
void shouldThrowCustomExceptionWhenServiceReturns500ServerErrorWithNoBody() {
setStubForInValidCheckCall(HttpStatus.INTERNAL_SERVER_ERROR,null);
Throwable thrown =
catchThrowable(() -> myClient.performComplianceCheck(getCompany()));
assertThat(thrown)
.isInstanceOf(MyCustomServiceException.class)
.hasMessageContaining("service returned status 500");
}
I am struggling to understand why this happens, and how to fix it..
is it "normal" ? or am I missing something obvious (is it a problem with my test ?) ?
I have found a workaround, but it doesn't feel "webFlux-y" at all, and I still don't understand why the Only one connection receive subscriber allowed was happening :
try {
ResponseEntity<String> responseEntity =
webClient.get()
.uri(targetServiceUri)
.retrieve()
.toEntity(String.class)
.block();
}
catch (WebClientException e) {
if(e instanceof InternalServerError){
var internalServerError=(InternalServerError) e;
if(internalServerError.getStatusCode().is5xxServerError()){
var respBody=internalServerError.getResponseBodyAsString();
if(StringUtils.isEmpty(respBody)){
respBody=MY_STANDARD_MESSAGE +internalServerError.getRawStatusCode() ;
}
throw new MyCustomServiceException(respBody);
}
}
}

Getting the response body in error case with Spring WebClient

I am trying to replace the existing client code with RestTemplate with a WebClient. For that reason, most of the calls need to be blocking, so that the main portion of the application does not need to change. When it comes to error handling this poses a bit of a problem. There are several cases that have to be covered:
In a successful case the response contains a JSON object of type A
In an error case (HTTP status 4xx or 5xx) the response may contain a JSON object of type B
On certain requests with response status 404 I need to return an empty List matching the type of a successful response
In order to produce the correct error (Exception) the error response needs to be considered. So far I am unable to get my hands on the error body.
I am using this RestController method to produce the error response:
#GetMapping("/error/404")
#ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity error404() {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse());
}
With this response object:
public class ErrorResponse {
private String message = "Error message";
public String getMessage() {
return message;
}
}
The WebClient is defined as follows:
WebClient.builder()
.baseUrl("http://localhost:8081")
.clientConnector(connector)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
With the connector being of type CloseableHttpAsyncClient (Apache Http client5).
From my test application I make the call like this:
public String get(int httpStatus) {
try {
return webClient.get()
.uri("/error/" + httpStatus)
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> {
clientResponse.bodyToMono(String.class).flatMap(responseBody -> {
log.error("Body from within flatMap within onStatus: {}", responseBody);
return Mono.just(responseBody);
});
return Mono.error(new RuntimeException("Resolved!"));
})
.bodyToMono(String.class)
.flatMap(clientResponse -> {
log.warn("Body from within flatMap: {}", clientResponse);
return Mono.just(clientResponse);
})
.block();
} catch (Exception ex) {
log.error("Caught Error: ", ex);
return ex.getMessage();
}
}
What I get is the RuntimeException from the onStatus return and of course the caught exception in the end.
I am missing the processing from the bodyToMono from within the onStatus. My suspicion is that this is not executed due to the blocking nature, as the response body is dealt with the bodyToMono after the onStatus.
When commenting out the onStatus I would expect that we log the warning in the flatMap, which does not happen either.
In the end I would like to define the handling of errors as a filter so that the code does not need to be repeated on every call, but I need to get the error response body, so that the exception can be populated with the correct data.
How can I retrieve the error response in a synchronous WebClient call?
This question is similar to Spring Webflux : Webclient : Get body on error, which has no accepted answer and some of the suggested approaches use methods that are no deprecated.
Here is one approach to handle error responses:
use onStatus to capture error http status
deserialize error response clientResponse.bodyToMono(ErrorResponse.class)
generate new error signal based on the error response Mono.error(new RuntimeException(error.getMessage())). Example uses RuntimeException but I would suggest to use custom exception to simplify error handling downstream.
webClient.get()
.uri("/error/" + httpStatus)
.retrieve()
.onStatus(HttpStatus::isError, clientResponse ->
clientResponse.bodyToMono(ErrorResponse.class)
.flatMap(error ->
Mono.error(new RuntimeException(error.getMessage()))
)
)
.bodyToMono(Response.class)
You don't really need try-catch. If you block the above code would return Response in case of the non-error response and throws exception with custom message in case of error response.
Update
Here is a full test using WireMock
class WebClientErrorHandlingTest {
private WireMockServer wireMockServer;
#BeforeEach
void init() {
wireMockServer = new WireMockServer(wireMockConfig().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
}
#Test
void test() {
stubFor(post("/test")
.willReturn(aResponse()
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withStatus(400)
.withBody("{\"message\":\"Request error\",\"errorCode\":\"10000\"}")
)
);
WebClient webClient = WebClient.create("http://localhost:" + wireMockServer.port());
Mono<Response> request = webClient.post()
.uri("/test")
.retrieve()
.onStatus(HttpStatus::isError, clientResponse ->
clientResponse.bodyToMono(ErrorResponse.class)
.flatMap(error ->
Mono.error(new RuntimeException(error.getMessage() + ": " + error.getErrorCode()))
)
)
.bodyToMono(Response.class);
RuntimeException ex = assertThrows(RuntimeException.class, () -> request.block());
assertEquals("Request error: 10000", ex.getMessage());
}
#Data
private static class ErrorResponse {
private String message;
private int errorCode;
}
#Data
private static class Response {
private String result;
}
}

Spring 5 webflux inter-dependent webclient calls are not getting timeout

I have two interdependent webclient calls to different api's , when the first webclient call response is delayed then readtimeout excpetion is coming . but whenever the first call is success and the second call response is delayed then it is waiting for response indefinitely.
i tried creating seperate instances of webclient for each call . still issue persists.
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10))));
return WebClient.builder().baseUrl(url).clientConnector(new ReactorClientHttpConnector(httpClient))
.exchangeStrategies(ExchangeStrategies.withDefaults())
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
.filter(ExchangeFilterFunctions
.basicAuthentication(", "))
.build();
Two subsequent calls are below where , when the second call is delayed response then the readtimeout exception is not thrown
request = request.flatMap(req ->
tempService.getId(loggedInUser, token)
.map(response -> {
req.setRetrieveClientIdentifier(response.getId());
return seRequest;
}))
.zipWhen(request -> tempService.getIdFor(request.getIdentifier(), accountToken)).map(tuple -> {
tuple.getT1().setID(tuple.getT2().getId());
return tuple.getT1();
});

Categories

Resources