How do I handle WebClient exceptions when using block()? - spring-webflux

I have a service written using spring web. However, to perform our http calls we have decided to use WebClient (I do not have the option to use RestTemplate or convert the service to use WebFlux). So, in the block code below:
public List<SomeObject> getObject(final String param1, final String param2) {
return openApiGenerateClient.getSomething(param1, param2)
.collectList()
.onErrorResume(WebClientResponseException.BadRequest.class, t -> error(new CustomException(t)))
.block();
}
I have a client side code generated by openApi which uses WebClient to perform the http request. The response is a Flux. I would like to "catch" specific errors, such as "Bad Request" and convert into my custom error to be handled in my Controller Advice. This code above although it compiles, when tested always return the exception of type WebClientResponseException.BadRequest and not my CustomException.
I have tried other Flux operations such as .onErrorReturn . I know I can simply wrap my call in a try and catch and then handle the errors, but I would like to know if there is a cleaner way of doing, without having some ugly code with multiple catchs.

I didn't tested it but it seems that should catch the errors before doing the collectList.
public List<SomeObject> getObject(final String param1, final String param2) {
return openApiGenerateClient.getSomething(param1, param2)
.onErrorResume(WebClientResponseException.BadRequest.class, t -> error(new CustomException(t)))
.collectList()
.block();
}
By doing this you expect the error to be throwed by the getSomething call, instead of expecting it in the collectList()

Related

Mono.zip request fails when subscribed

I have the following piece of code, where externalgetcall is a GET request to a external service asking for some data
myservice.externalgetcall(id).blockOptional();
this code works, but if i get rid of the blockOptional and write the following, externalgetcall fails with a java.lang.NullPointerException:
java.lang.NullPointerException: null
at org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository.saveAuthorizedClient(HttpSessionOAuth2AuthorizedClientRepository.java:63) ~[spring-security-oauth2-client-5.7.2.jar:5.7.2]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ? Request to GET
myservice.externalgetcall(id).subscribe();
moreover, if I do this, the blocking one right before the non blocking one, it works, so it clearly has to do with Oauth not completing somehow if the call is non blocking:
myservice.externalgetcall(id).blockOptional();
myservice.externalgetcall(id).subscribe();
externalgetcall(id)
public Mono<MyClass> externalgetcall(String id) {
logger.debug("getting contact: {}", id);
return this.webClient
.get()
.uri(externaluri)
.retrieve()
.bodyToMono(MyClass.class)
.doOnNext(myClass -> logger.debug("success {}", myClass))
.doOnError(throwable -> logger.error("error : ", throwable))
}
it fails at this point:
it looks like it happens when trying to run the setAttribute method,when debugging i can see this:
this = {FluxSubscribeOnCallable$CallableSubscribeOnSubscription#13199} size = 1
Unable to evaluate the expression Method threw 'java.lang.UnsupportedOperationException' exception.
public final class HttpSessionOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository {
private static final String DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME = HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS";
private final String sessionAttributeName;
public HttpSessionOAuth2AuthorizedClientRepository() {
this.sessionAttributeName = DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME;
}
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, HttpServletRequest request, HttpServletResponse response) {
if (this.isPrincipalAuthenticated(principal)) {
this.authorizedClientService.saveAuthorizedClient(authorizedClient, principal);
} else {
this.anonymousAuthorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, request, response);
}
}
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, HttpServletRequest request, HttpServletResponse response) {
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
Map<String, OAuth2AuthorizedClient> authorizedClients = this.getAuthorizedClients(request);
authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient);
request.getSession().setAttribute(this.sessionAttributeName, authorizedClients);
}
This is not much context to come to a rock-solid explanation, but the NPE leads me to believe that this is a case of race condition.
Something to consider is the fact that you are replacing a blocking terminal operation with an asynchronous terminal operation on the publisher. blockOptional and subscribe both initiate a subscription, but in the latter case, execution will not wait at that point for the publisher's onComplete signal.
Again, hard to tell without the complete code, but my guess is that whatever code comes after this snippet is using some data that is populated or hydrated as a result of this publisher. Using blockOptional ensures that the publisher completes before this happens, while subscribe does not.

Handling checked exception in Mono flow

Not sure how to handle checked exception in the Mono flow.
return Mono.when(monoPubs)
.zipWhen((monos) -> repository.findById(...))
.map((tuple) -> tuple.getT2())
.zipWhen((org) -> createMap(org))
.map((tuple) -> tuple.getT2())
.zipWhen((map) -> emailService.sendEmail(...))
.flatMap(response -> {
return Mono.just(userId);
});
Here, the sendEmail method is declared with throws Exception.
public Mono<Boolean> sendEmail(...)
throws MessagingException, IOException
So, How to handle this checked exception in the zipWhen flow.
Also, How to handle
.zipWhen((map) -> emailService.sendEmail(...))
if the method returns void.
You need to review implementation of the sendEmail. You cannot throw checked exceptions from the publisher and need to wrap any checked exception into an unchecked exception.
The Exceptions class provides a propagate method that could be used to wrap any checked exception into an unchecked exception.
try {
...
}
catch (SomeCheckedException e) {
throw Exceptions.propagate(e);
}
As an alternative, you could use lombok #SneakyThrows to wrap non-reactive method.
Exceptions are thrown from Mono method, you can use onError* methods to handle the exception the way you like
var result = Mono.just("test")
.zipWhen((map) -> sendEmail())
.onErrorMap(SendEmailException.class, e -> new RuntimeException(e.getMessage()))
.flatMap(Mono::just);
Also it is not very clear from your post that if sendEmail takes param or not, sendEmail is not taking any input, I would just use doOnNext as it is a void method.

Spring Webflux returning null back to controller

Every time I think I understand Webflux and project reactor, I find out I have no idea.
So I making some API calls... I want to call 1 first ... Get information back use that information, to make subsequent calls.
so I do this like so
public Mono<ResponseObject> createAggregatedRecords(RecordToPersist recordToPersist){
return randomApiClient.createRecord(recordToPersist)
.flatMap(result -> {
return Mono.zip(
webClientInstance.createOtherRecord1(result.getChildRecord1()),
webClientInstance2.createOtherRecord2(result.getChildRecord2()),
webClientInstance3.createOtherRecord3(result.getChildRecord3()))
.map(tupple -> {
ResponseObject respObj = new ResponseObject();
respObj.setChildResult1(tupple.getT1());
respObj.setChildResult2(tupple.getT2());
respObj.setChildResult3(tupple.getT3());
return respObj;
}
}).doOnSuccess(res -> log.info("This throws an error: {}", res.getChildResult1.getFirstField()))
}
Now, for some reason, I am returning a null object with this very code to my Controller and I am not printing out the object in Json.
I suspect it is because I am nesting the Mono.zip inside the flatmap, and am not returning the results back correctly. I am making all of those API calls though as my End-to-End integration tests are succeeding.
Now I thought that I would return that response object from the .map function from the Mono.zip chain and then return that to the flatMap call in the chain. If I put observers on the chain like a doOnSuccess and print out response object fields I get a null pointer ... Not sure what I am missing
Is this a good pattern to achieve that goal? Or should I try a different path?
Why can I not get the response Object to return?

WebFlux: Only one item arriving at the backend

On the backend im doing:
#PostMapping(path = "/products", consumes = MediaType.APPLICATION_STREAM_JSON_VALUE)
public void saveProducts(#Valid #RequestBody Flux<Product> products) {
products.subscribe(product -> log.info("product: " + product.toString()));
}
And on the frontend im calling this using:
this.targetWebClient
.post()
.uri(productUri)
.accept(MediaType.APPLICATION_STREAM_JSON)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(this.sourceWebClient
.get()
.uri(uriBuilder -> uriBuilder.path(this.sourceEndpoint + "/id")
.queryParam("date", date)
.build())
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToFlux(Product.class), Product.class)
.exchange()
.subscribe();
What happens now is that I have 472 products which need to get saved but only one of them is actually saving. The stream closes after the first and I cant find out why.
If I do:
...
.retrieve()
.bodyToMono(Void.class);
instead, the request isnt even arriving at the backend.
I also tried fix amount of elements:
.body(Flux.just(new Product("123"), new Product("321")...
And with that also only the first arrived.
EDIT
I changed the code:
#PostMapping(path = "/products", consumes =
MediaType.APPLICATION_STREAM_JSON_VALUE)
public Mono<Void> saveProducts(#Valid #RequestBody Flux<Product> products) {
products.subscribe(product -> this.service.saveProduct(product));
return Mono.empty();
}
and:
this.targetWebClient
.post()
.uri(productUri)
.accept(MediaType.APPLICATION_STREAM_JSON)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(this.sourceWebClient
.get()
.uri(uriBuilder -> uriBuilder.path(this.sourceEndpoint + "/id")
.queryParam("date", date)
.build())
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToFlux(Product.class), Product.class)
.exchange()
.block();
That led to the behaviour that one product was saved twice (because the backend endpoint was called twice) but again only just one item. And also we got an error on the frontend side:
IOException: Connection reset by peer
Same for:
...
.retrieve()
.bodyToMono(Void.class)
.subscribe();
Doing the following:
this.targetWebClient
.post()
.uri(productUri)
.accept(MediaType.APPLICATION_STREAM_JSON)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(this.sourceWebClient
.get()
.uri(uriBuilder -> uriBuilder.path(this.sourceEndpoint + "/id")
.queryParam("date", date)
.build())
.accept(MediaType.APPLICATION_STREAM_JSON)
.retrieve()
.bodyToFlux(Product.class), Product.class)
.retrieve();
Leads to the behaviour that the backend again isnt called at all.
The Reactor documentation does say that nothing happens until you subscribe, but it doesn't mean you should subscribe in your Spring WebFlux code.
Here are a few rules you should follow in Spring WebFlux:
If you need to do something in a reactive fashion, the return type of your method should be Mono or Flux
Within a method returning a reactive typoe, you should never call block or subscribe, toIterable, or any other method that doesn't return a reactive type itself
You should never do I/O-related in side-effects DoOnXYZ operators, as they're not meant for that and this will cause issues at runtime
In your case, your backend should use a reactive repository to save your data and should look like:
#PostMapping(path = "/products", consumes = MediaType.APPLICATION_STREAM_JSON_VALUE)
public Mono<Void> saveProducts(#Valid #RequestBody Flux<Product> products) {
return productRepository.saveAll(products).then();
}
In this case, the Mono<Void> return type means that your controller won't return anything as a response body but will signal still when it's done processing the request. This might explain why you're seeing that behavior - by the time the controller is done processing the request, all products are not saved in the database.
Also, remember the rules noted above. Depending on where your targetWebClient is used, calling .subscribe(); on it might not be the solution. If it's a test method that returns void, you might want to call block on it and get the result to test assertions on it. If this is a component method, then you should probably return a Publisher type as a return value.
EDIT:
#PostMapping(path = "/products", consumes =
MediaType.APPLICATION_STREAM_JSON_VALUE)
public Mono<Void> saveProducts(#Valid #RequestBody Flux<Product> products) {
products.subscribe(product -> this.service.saveProduct(product));
return Mono.empty();
}
Doing this isn't right:
calling subscribe decouples the processing of the request/response from that saveProduct operation. It's like starting that processing in a different executor.
returning Mono.empty() signals Spring WebFlux that you're done right away with the request processing. So Spring WebFlux will close and clean the request/response resources; but your saveProduct process is still running and won't be able to read from the request since Spring WebFlux closed and cleaned it.
As suggested in the comments, you can wrap blocking operations with Reactor (even though it's not advised and you may encounter performance issues) and make sure that you're connecting all the operations in a single reactive pipeline.

Transforming a Spring Webflux Mono to an Either, preferably without blocking?

I'm using Kotlin and Arrow and the WebClient from spring-webflux. What I'd like to do is to transform a Mono instance to an Either.
The Either instance is created by calling Either.right(..) when the response of the WebClient is successful or Either.left(..) when WebClient returns an error.
What I'm looking for is a method in Mono similar to Either.fold(..) where I can map over the successful and erroneous result and return a different type than a Mono. Something like this (pseudo-code which doesn't work):
val either : Either<Throwable, ClientResponse> =
webClient().post().exchange()
.fold({ throwable -> Either.left(throwable) },
{ response -> Either.right(response)})
How should one go about?
There is no fold method on Mono but you can achieve the same using two methods: map and onErrorResume. It would go something like this:
val either : Either<Throwable, ClientResponse> =
webClient().post()
.exchange()
.map { Either.right(it) }
.onErrorResume { Either.left(it).toMono() }
I'm not really familiar with that Arrow library nor the typical use case for it, so I'll use Java snippets to make my point here.
First I'd like first to point that this type seems to be blocking and not lazy (unlike Mono). Translating a Mono to that type means that you'll make your code blocking and that you shouldn't do that, for example, in the middle of a Controller handler or you will block your whole server.
This is more or less the equivalent of this:
Mono<ClientResponse> response = webClient.get().uri("/").exchange();
// blocking; return the response or throws an exception
ClientResponse blockingResponse = response.block();
That being said, I think you should be able to convert a Mono to that type by either calling block() on it and a try/catch block around it, or turning it first into a CompletableFuture first, like:
Mono<ClientResponse> response = webClient.get().uri("/").exchange();
Either<Throwable, ClientResponse> either = response
.toFuture()
.handle((resp, t) -> Either.fold(t, resp))
.get();
There might be better ways to do that (especially with inline functions), but they all should involve blocking on the Mono in the first place.