webclient retrieve() vs exchangeToMono() nothing working for this use case - java

API (Name it as api-1) has following property -
For 2XX it can return body
For 5XX it doesn’t return body
In another API (api-2), Our requirement is if api-1 status code is 2XX return “ABC” else return “XYZ” from webclient call, So we don’t wanted to consume response body in either cases.
Which one should work retrieve() or exchangeToMono() ?
When I use retrieve() I couldn’t return ABC or XYZ based on http response. If I use exchangeToMono() I was getting InvalidStateException with following message “The underlying HTTP client completed without emitting a response.”
Any help is appreciated.

Here is my approach.
String result = client
.get()
.uri("http://localhost:8080/endpoint")
.retrieve()
.toBodilessEntity()
.onErrorResume(WebClientResponseException.class, e -> Mono.just(ResponseEntity.status(e.getStatusCode()).build()))
.map(entity -> entity.getStatusCode().isError() ? "error" : "ok")
.block();
result is equal to ok when the request is completed successfully, and error otherwise.
You can change the condition in map if you want to return error only on 5xx exceptions.
EDIT: Or as Toerktumlare mentioned in his comment, you can use onStatus to omit an exception.
String result = client
.get()
.uri("http://localhost:8080/endpoint")
.retrieve()
.onStatus(HttpStatus::isError, e -> Mono.empty())
.toBodilessEntity()
.map(entity -> entity.getStatusCode().isError() ? "error" : "ok")
.block();

Related

Update authorization header in flux WebClient

I'm trying to create a resilient sse (server sent event) client in reactive programming.
The sse endpoint is authenticated, therefore I have to add an authorization header to each request.
The authorization token expires after 1 hour.
Below is a snippet of my code
webClient.get()
.uri("/events")
.headers(httpHeaders -> httpHeaders.setBearerAuth(authService.getIdToken()))
.retrieve()
.bodyToFlux(ServerSideEvent.class)
.timeout(Duration.ofSeconds(TIMEOUT))
.retryWhen(Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(RETRY_DELAY)))
.subscribe(
content -> {
handleEvent(content);
},
error -> logger.error("Error receiving SSE: {}", error),
() -> logger.info("Completed!!!"));
If after 1 hour the connection is lost for any reason, this code stops working because the token is expired.
How can I refresh the token into the retry logic or in some other way?
Thank you
You can use webclient filter.
A filter can intercept and modify client request and response.
Example:
WebClient.builder().filter((request, next) -> {
ClientRequest newReuqest = ClientRequest.from(request)
.header("Authorization", "YOUR_TOKEN")
.build();
return next.exchange(newRequest);
}).build();
UPDATE:
Sorry, Not read your question clearly. Try this:
Assume that server return 401 code when token expired.
WebClient.builder().filter((request, next) -> {
final Mono<ClientResponse> response = next.exchange(request);
return response.filter(clientResponse -> clientResponse.statusCode() != HttpStatus.UNAUTHORIZED)
// handle 401 Unauthorized (token expired)
.switchIfEmpty(next.exchange(ClientRequest.from(request)
.headers(httpHeaders -> httpHeaders.setBearerAuth(getNewToken()))
.build()));
}).build();
Or you can cache your token (e.g. Save to redis and set TTL in one hour), when the token is empty from redis, get new one then save to redis again.

Avoid refreshing and saving token multiple times in a cache during parallel https calls to service

I am currently in the process of replacing the blocking HTTP calls from one of my projects with Asynchronous/reactive calls. The saveOrderToExternalAPI is an example of the refactor I have done that replaced the blocking HTTP call and now using reactive WebClient.
The function saveOrderToExternalAPI calls an endpoint and returns an empty MONO in case of success. It also retries when the API returns 401 Unauthorized error which happens when the token is expired. If you notice the retryWith logic, you will see the code is renewing the token (tokenProvider.renewToken()) before actually retrying.
The tokenProvider.renewToken() for now is a blocking call that fetches a new token from another endpoint and saves it to a Cache so that we don't have to renew the token again and subsequent calls can just reuse it. Whereas the tokenProvider.loadCachedToken() function check and return token if it is not Null, otherwise renew and then return it.
public Mono<ResponseEntity<Void>> saveOrderToExternalAPI(Order order) {
log.info("Sending request to save data.");
return webclient.post()
.uri("/order")
.headers(httpHeaders -> httpHeaders.setBearerAuth(tokenProvider.loadCachedToken()))
.accept(MediaType.APPLICATION_JSON)
.bodyValue(order)
.retrieve()
.toBodilessEntity()
.doOnError(WebClientResponseException.class, logHttp4xx)
.retryWhen(unAuthorizedRetrySpec())
.doOnNext(responseEntity -> log.debug("Response status code: {}",
responseEntity.getStatusCodeValue()));
}
private RetryBackoffSpec unAuthorizedRetrySpec() {
return Retry.fixedDelay(1, Duration.ZERO)
.doBeforeRetry(retrySignal -> log.info("Renew token before retrying."))
.doBeforeRetry(retrySignal -> tokenProvider.renewToken())
.filter(throwable -> throwable instanceof WebClientResponseException.Unauthorized)
.onRetryExhaustedThrow(propagateException);
}
private final Consumer<WebClientResponseException> logHttp4xx = e -> {
if (e.getStatusCode().is4xxClientError() && !e.getStatusCode().equals(HttpStatus.UNAUTHORIZED)) {
log.error("Request failed with message: {}", e.getMessage());
log.error("Body: {}", e.getResponseBodyAsString());
}
}
// -------- tokenProvider service
public String loadCachedToken() {
if (token == null) {
renewToken();
}
return token;
}
Everything is working fine because for now, the saveOrderToExternalAPI is eventually doing a blocking call in the Service layer.
for (ShipmentRequest order: listOfOrders) {
orderHttpClient.saveOrderToExternalAPI(order).block();
}
But, I would like this to be changed and refactor the service layer. I am wondering would happen if I keep using the existing logic of token renewal and process the saveOrderToExternalAPI parallelly as below.
List<Mono<ResponseEntity<Void>>> postedOrdersMono = listOfOrders.stream()
.map(orderHttpClient::saveOrderToExternalAPI)
.collect(Collectors.toList());
Mono.when(postedOrdersMono).block();
Now the calls will be done parallelly and the threads would try to update token cache at the same time (in case of 401 unauthorized) even though updating it once is enough. I am really new to reactive and would like to know if there's any callback or configuration that can fix this issue?

Response body in reactive spring

I have a problem with getting a response body with a reactive spring. The service that I'm integrating with can have a different JSON body in response, so I cannot have a DTO that I can map the response to, so I was trying with String.
To check the solution for now I have mocked a simple server with SoapUi to return code 400 and JSON :
{
"errorCode" : "code",
"errorMessage" : "message"
}
and I would like to be able to log in in console in case of having an error, so here is part of my code:
.bodyValue(eventDataDto)
.exchange()
.doOnSuccess((response) -> {
if (response.rawStatusCode() != HttpStatus.OK.value()) {
throw new RuntimeException("Sending request failed with status: " + response.rawStatusCode() +
" with error message: " + response.bodyToMono(String.class);
}
})
.doOnError((throwable) -> {
throw new RuntimeException(throwable);
})
.block();
but the response that I'm getting is:
Sending request failed with status: 400 with error message: checkpoint("Body from POST http://localhost:8095/test/ [DefaultClientResponse]")
I was trying to use subscribe, map, and flatMap on that mono, but the only result was that I was getting a different class type on that error message, but nothing close to what I'm looking for.
I would appreciate any help with getting the response body in any form, that I could use its content.
EDIT WITH ANSWER:
A minute after posting it I have found a solution:
So I have tried with subscribe on mono:
.bodyValue(eventDataDto)
.exchange()
.doOnSuccess((response) -> {
if (response.rawStatusCode() != HttpStatus.OK.value()) {
Mono<String> bodyMono = response.bodyToMono(String.class);
bodyMono.subscribe(body -> {
throw new RuntimeException(
"Sending request failed with status: " + response.rawStatusCode() +
" with error message: " + body);
});
}
})
.doOnError((throwable) -> {
throw new RuntimeException(throwable);
})
.block();
I completly forgot that Mono will not give a result until subscribed.
You are receiving the proper result as your HTTP call succeeds (go-through) but returns a 400 ~ Bad Request response code which is a full concise HTTP response (which is the correct response you have configured as per your description in the OP).
In order to have the response body retrieved, either being the proper response body on a successful (200) or unsuccessful (400) response, you should use:
Either the toEntity operator
Or the bodyToMono one
Here down how your call chain would look like mapping the error response content using bodyToMono:
webClient.get()
.uri("some-uri")
.bodyValue(eventDataDto)
.exchange()
.flatMap((response) -> {
if (response.rawStatusCode() != HttpStatus.OK.value()) {
return response.bodyToMono(String.class)
.map(errorContent -> "Sending request failed with status: " + response.rawStatusCode() + " with error message: " + errorContent);
} else {
return response.bodyToMono(String.class);
})
.block();

How add or Ignoring a specific http status code in WebClient reactor?

I have a this method that manage all request of my project:
private Mono<JsonNode> postInternal(String service, String codPayment, String docNumber, RequestHeadersSpec<?> requestWeb) {
return requestWeb.retrieve().onStatus(HttpStatus::is4xxClientError,
clientResponse -> clientResponse.bodyToMono(ErrorClient.class).flatMap(
errorClient -> clientError(service, codPayment, clientResponse.statusCode(), docNumber, errorClient)))
.onStatus(HttpStatus::is5xxServerError,
clientResponse -> clientResponse.bodyToMono(ErrorClient.class)
.flatMap(errorClient -> serverError(service, codPayment, docNumber, errorClient)))
.onRawStatus(value -> value > 504 && value < 600,
clientResponse -> clientResponse.bodyToMono(ErrorClient.class)
.flatMap(errorClient -> otherError(service, codPayment, docNumber, errorClient)))
.bodyToMono(JsonNode.class);
}
But one of the API that i consume response with Status code 432 when the response is ok but have a special condition in it and i must show it but webClient show this error:
org.springframework.web.reactive.function.client.UnknownHttpStatusCodeException: Unknown status code [432]
at org.springframework.web.reactive.function.client.DefaultClientResponse.lambda$createException$1(DefaultClientResponse.java:220) ~[spring-webflux-5.2.1.RELEASE.jar:5.2.1.RELEASE]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ 432 from POST https://apiThatIConnectTo....
How can i avoid this and response normally? and is possible to add this status code to my JsonNode.class or a custom generic class that map the response and the status code or any ideas? thanks
From the java doc. (This also applies to the onRawStatus method)
To suppress the treatment of a status code as an error and process it as a normal response, return Mono.empty() from the function. The response will then propagate downstream to be processed.
An example code snippet would look like
webClient.get()
.uri("http://someHost/somePath")
.retrieve()
.onRawStatus(status -> status == 432, response -> Mono.empty())
.bodyToMono(String.class);

Java Vavr : Log Exception on Failure

I want to log exceptions while using VAVR (formerly javaslang). Below is the sample code snippet.
//will get 500 as response from this url
String sampleurl = "http://someurl.com";
List<String> myList = List.of(sampleurl);
LOGGER.info("In {} with urls {}",getClass(),myList);
return Observable.from(myList).flatMap(url ->
Observable.create(subscriber -> {
Try<String> httpEntity = HttpUtil.retrieveData(url).flatMap(httpResponse -> Try.of( () -> EntityUtils.toString(httpResponse.getEntity())));
httpEntity
.andThen(subscriber::onNext)
.andThen(subscriber::onCompleted)
.onFailure(subscriber::onError);
}));
I am trying to log exception in the onFailure() block but nothing gets logged. Please advise me on this.
Regards,
Jai
In both cases, success and failure, Vavr works as expected. Here are simple tests:
// prints nothing
Try.success("ok")
.andThen(() -> {})
.andThen(() -> {})
.onFailure(System.out::println);
// prints "java.lang.Error: ok"
Try.failure(new Error("ok"))
.andThen(() -> {})
.andThen(() -> {})
.onFailure(System.out::println);
I see two possible answers why the failure is not logged in your example:
the logger configuration does not fit your needs
the observables are not processed and Try is never called
Disclamer: I'm the creator of Vavr

Categories

Resources