Repeat on a Mono with Spring reactive webClient until specific response - java

The case is the following:
There is an API endpoint that returns as response "pending" or "pompleted".
I want to repeatedly call this, let's say every 10 seconds, until I get a "Completed" response. If it keeps responding "Pending" for 5 minutes, I want a timeout.
I've read about repeat, repeatWhen and repeatWhenEmpty, but I can't get it done.
An example of my code is the following :
String getStatusResponse = webClient
.get()
.uri(getStatusUri)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.filter(response -> equals("completed"))
.repeatWhenEmpty(Repeat.onlyIf(r -> false)
.fixedBackoff(Duration.ofSeconds(10))
.timeout(Duration.ofMinutes(5)))
.block();
Edit after #Michael McFadyen's comment :
My code is now the following :
GenerationApiGetStatus getStatusResponse = webClient
.get()
.uri(getStatusUri)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(GenerationApiGetStatus.class)
.filter(response -> response.getStatus().equals("completed"))
.repeatWhenEmpty(Repeat.onlyIf(r -> true)
.fixedBackoff(Duration.ofSeconds(10))
.timeout(Duration.ofMinutes(5)))
.block();
GenerationApiGetStatus has a property "status" for JSON unmarshalling.
The problem is that I keep receiving null. If a change the code to just get once the status and go on, I correctly receive "Pending".
I believe something is wrong with the filtering :
.filter(response -> response.getStatus().equals("completed"))
Any ideas?

From the Api Docs of Retry.onlyIf
Repeat function that repeats only if the predicate returns true.
In your code sample you are always returning false
Repeat.onlyIf(r -> false)
As a result, the repeat will never happen.
You can alter your Retry to the code below to get your desired behaviour.
Repeat.onlyIf(r -> true)
.fixedBackoff(Duration.ofSeconds(10))
.timeout(Duration.ofMinutes(5))

Related

Spring Web Client - How to get response string of failed request wrapped intro retrials

A little background
I would like to call service's APIs, while doing retries on 5xx errors. Also, I would like to get an access to every failed request (for logging purposes).
Code
getClient()
.get()
.uri("http://example.com")
.retrieve()
.onStatus(HttpStatus::is5xxServerError, rsp -> Mono.error(new ApiServerException("Server error", rsp.rawStatusCode())))
.bodyToMono(ReportListResponse.class)
.retryWhen(
Retry
.backoff(3, Duration.ofSeconds(1))
.filter(throwable -> throwable instanceof ApiServerException)
)
.block();
Issue
How can I achieve the goal of being able to access the response of every failed request? I was trying to retrieve the body while using rsp.bodyToMono(String.class) in onStatus method. Unfortunately it didn't give me an expected output.
You would need to use response.bodyToMono in the onStatus to get response body. The following example shows how to deserialize body into String but you could define POJO as well.
getClient()
.get()
.uri("http://example.com")
.retrieve()
.onStatus(HttpStatus::isError, response ->
response.bodyToMono(String.class)
.doOnNext(responseBody ->
log.error("Error response from server: {}", responseBody)
)
// throw original error
.then(response.createException())
)
.bodyToMono(ReportListResponse.class)
}

Latest Spring Boot [2.5.0] webclient bug?

I have recently upgraded my project to the latest version of spring-boot 2.5.0 and got going with refactoring a ton of deprecated code. I noticed that awaitExchange() has now been deprecated and should be replaced with awaitExchange{it}
However, as soon as I replaced one with the other it appears I can no longer extract the body from the ClientResponse object by response.awaitBody() in a different class and keep getting No value received via onNext for awaitSingle. Is such behaviour by design?
Is there any other way to actually get hold of the body without having to use `
awaitExchange{ it.awaitBody() } in the class that makes the webservice call?
Since you did not show your code its hard to say what is the issue. But you can use WebClient in following ways
val client = WebClient.create()
val data: MultiValueMap<String, String> = LinkedMultiValueMap()
data["username"] = "johndoe"
data["target_site"] = "aloha"
client.create()
.post()
.uri("some uri")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(data))
.retrieve()
.awaitBodyOrNull<String>() ?: throw Exception("Received null response")
Another way to do is
val response = client.get()
.uri("some uri")
.contentType(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(String::class.java)
.awaitSingle()
if (!response.statusCode.is2xxSuccessful) {
throw Exception("Received ${response.statusCodeValue} response.")
}

Short polling using WebFlux

I have this post method:
webClientBuilder
.build()
.get()
.uri("uri")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::isError, response -> Mono.error(new CustomException("Response is in status: ".concat(response.statusCode().toString()))))
.bodyToMono(GetResponse.class)
.log()
.flatMap(response ->
Mono.fromSupplier(() -> updateMember(entity.getId(), getScore(response)))
.subscribeOn(Schedulers.boundedElastic()))
.block();
I should create a short poll on it, since the api I called could delay answering me, I should be able to call it several times, maybe every 3/5 seconds. The external service always gives me an answer, but I have to verify that a specific field in the answer is not null. I have to repeat it at most 5 times, if after 5 times it returns null, I will pass null to my method (updateMember).
I am trying something like this:
webClientBuilder
.build()
.get()
.uri("uri")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::isError, response -> Mono.error(new CustomException("response is in status: ".concat(response.statusCode().toString()))))
.bodyToMono(GetResponse.class)
.filter(response -> Objects.nonNull(response.getAnag().getSummary()))
.repeatWhenEmpty(Repeat.onlyIf(repeatContext -> true)
.exponentialBackoff(Duration.ofSeconds(5), Duration.ofSeconds(10)).timeout(Duration.ofSeconds(30)))
.delaySubscription(Duration.ofSeconds(10))
.flatMap(response -> Mono.fromSupplier(() -> updateMember(entity.getId(), response.getAnag().getSummary()))
.subscribeOn(Schedulers.boundedElastic()))
.block();
I would like to repeat at most 5 times, only if that value in the answer is null otherwise I pass the value directly.
Can you help me?
If you get a timeout exception because of the delay, you can add one of the '.retry()'s operators to the chain. Perhaps this will be helpful:
.retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(3)))
Example from reactor reference documentation

How to handle 500 error code using retrieve in webclient

I've read quite a few documentations and other stackoverflow questions regarding this matter but I can't seem to get my code working.
So essentially I have a WebClient making a POST request.
IF the response status is 200, then I make another call to another endpoint using a different WebClient. After second webclient call, return a string.
ELSE I just return a String from the method e.g. "failed to create order.".
Simple enough. (this is all done in a seperate thread fyi, not the main thread.)
But I've noticed that if i do get back a 500 error code, WebClient throws an exception. What I want to do is capture the exception and handle that gracefully and return a String like "Error calling first endpoint etc."
This is what I have so far:
private String generateOrder(ImportedOrderDetails importedOrderDetails)
{
Order requestBody = generateRequestBody(importedOrderDetails);
OrderResponse responseForCreatingOrder = orderWebClient()
.post()
.body(Mono.just(requestBody), Order.class)
.retrieve()
.bodyToMono(OrderResponse.class)
.block();
if (responseForCreatingOrder.getResponseStatus().equals(SUCCESS))
{...other call using different webclient}
else{ return "Error creating order."}
This works fine when the response status is 200 but when its 500 it blows up.
OrderResponse is a custom object. orderWebClient() is just a method that returns a prebuilt WebClient containing the baseUrl and headers etc.
I came across this:
Spring WebClient - How to handle error scenarios I did try implementing it but couldn't figure out where to put the block method since I kept on getting the following:
reactor.core.Exceptions$ReactiveException: java.lang.Exception
at reactor.core.Exceptions.propagate(Exceptions.java:393)
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:97)
at reactor.core.publisher.Mono.block(Mono.java:1680)
I had to edit my code a bit to try and implement the answer to that question:
private Mono<? extends Throwable> handleError(String message) {
log.error("====---"+message);
return Mono.error(Exception::new);
}
private String generateOrder(ImportedOrderDetails importedOrderDetails)
{
Order requestBody = generateRequestBody(importedOrderDetails);
Mono<OrderResponse> responseForCreatingDemo = orderWebClient()
.post()
.body(Mono.just(requestBody), Order.class)
.retrieve()
.onStatus(
(HttpStatus::is5xxServerError),
(it -> handleError(it.statusCode().getReasonPhrase()))
)
.bodyToMono(OrderResponse.class);
System.out.println("-=-"+responseForCreatingDemo);
if (responseForCreatingOrder != null && responseForCreatingOrder.block().getHeader().getResponseStatus().equals(SUCCESS)){...}
The error was coming from the .block part in the if condition. I believe this is something pretty trivial and missing the big picture.
Any suggestions?
It seems you have two kinds of statuses:
Http status, defined by the protocol itself (see HTTP response status codes)
Something specific to the application you're working on, encapsulated into the OrderResponse class.
So you have to handle two "errors" instead of one, one of the possible solutions might look like
.retrieve()
.bodyToMono(OrderResponse.class)
// 4xx, 5xx errors and return "Unable to create order" String instead
.onErrorContinue(WebClientResponseException.class, (ex, v) ->
Mono.just("Unable to create order"))
// if application specific status is not "ok" return "Unable to create order"
.map(it -> it.ok ? "Ok response" : "Unable to create order")
.block();
Please note that this code sample ignores exception and does not even log it

Spring 5 Webclient throw exception in doAfterSuccessOrError

I'm a java 7 developer (finally) taking his first steps in java 8. A lot of these things are still new to me. I'm trying to use the spring 5 WebClient since the documentation states RestTemplate will be moved away from in favor of WebClient.
webClient
.method(HttpMethod.POST)
.uri(uriBuilder -> uriBuilder.pathSegment("api", "payments").build())
.body(BodyInserters.fromObject(createPostRequest(paymentConfirmationData)))
.accept(MediaType.APPLICATION_JSON)
.exchange()
.doAfterSuccessOrError((clientResponse, throwable) -> {
if (clientResponse.statusCode().is5xxServerError()
|| clientResponse.statusCode().is4xxClientError()) {
logger.error("POST request naar orchestration layer mislukt, status: [{}]", clientResponse.bodyToMono(String.class));
Mono.error(throwable);
} else {
logger.error("POST request naar orchestration layer gelukt");
}
})
.block();
I'm trying to throw an exception in the .doAfterSuccesOrError. However I can't use throw throwable cause then it just adds a try catch around it. I read a few articles and this is my last attempt by adding Mono.error(throwable) but since there is no return I'm pretty sure this is the reason there is no effect.
This is a POST call that returns a 204 No Content on success. At the moment I'm getting a 422 although that shouldn't be important in this particular issue.
Can someone teach me how to throw exceptions back to the calling environment ?
There is a special method for handling the status codes. More here
Your code should look like
webClient.method(HttpMethod.POST)
.uri(uriBuilder -> uriBuilder.pathSegment("api", "payments").build())
.body(BodyInserters.fromObject(createPostRequest(paymentConfirmationData)))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxServerError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
...
.block();
Remember that when onStatus is used, if the response is expected to have content, then the onStatus callback should consume it. If not, the content will be automatically drained to ensure resources are released.
I ended up with the following code eventually
webClient
.method(HttpMethod.POST)
.uri(uriBuilder -> uriBuilder.pathSegment("api", "payments").build())
.body(BodyInserters.fromObject(createPostRequest(paymentConfirmationData)))
.accept(MediaType.APPLICATION_JSON)
.exchange()
.doOnSuccess((clientResponse) -> {
if (clientResponse.statusCode().is5xxServerError()
|| clientResponse.statusCode().is4xxClientError()) {
logger.error("POST request naar orchestration layer mislukt, status: [{}]", clientResponse.statusCode());
throw new RuntimeException("POST request naar orchestration layer mislukt");
} else {
logger.error("POST request naar orchestration layer gelukt");
}
})
.doOnError((throwable) -> {
logger.error("POST request naar orchestration layer mislukt");
throw new RuntimeException("POST request naar orchestration layer mislukt", throwable);
})
.block();
For those searching on how to approach exception and error handling. Have a look at this reference document on the Reactor Project: https://projectreactor.io/docs/core/release/reference/index.html#_error_handling_operators

Categories