Exception in not mapped inside flatMap - java

I have a piece of code that calls an external service. And I wanted to map error from this service. I tried to do that this way:
public Mono<Void> patch(String userId, Payload payload) {
return Mono.just(payload)
.flatMap(it -> client.patch(userId, PatchRequest.buildRequest(payload, userId))
.onErrorMap(throwable -> GeneralActionException.ofFailedSetup()));
}
But when I mocked the client to return RuntimeException
Mockito.when(client.patch(any(), any())).thenThrow(new RuntimeException());
It turned out my test:
StepVerifier.create(sut.patch(userId, payload))
.verifyError(GeneralActionException.class);
failed, because the returned error was RuntimeException:
However when I change the code a little, just like that:
public Mono<Void> patch(String userId, Payload payload) {
return Mono.just(payload)
.flatMap(it -> client.patch(userId, PatchRequest.buildRequest(payload, userId)))
.onErrorMap(throwable -> GeneralActionException.ofFailedSetup());
}
It turned out the test succeeded. The question is why? Because I don't understand why it works differently in both cases and especially why in the first example when error mapping is inside flatMap it doesn't map it to GeneralException?

Ok, I solved it. I mocked client wrongly.
Instead of:
Mockito.when(client.patch(any(), any())).thenThrow(new RuntimeException());
it should be:
Mockito.when(client.patch(any(), any())).thenReturn(Mono.error(RuntimeException::new));
as the client returns Mono<Void>

Related

Webflux returning Mono and using info in Object in Mono to set header

Let's say my Webflux handler returns a Mono on a product creation
That's easy to do.
But now, I want to complete the response with a location in the header.
To do so, I need to get the created product ID.
In my example, I used a block() which fails the reactive idea of the handler.
public Mono<ServerResponse> handleRequest(ServerRequest serverRequest) {
...
Mono<Product> monoProduct = // Service call to get the Mono<Product>
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.location(URI.create(String.format("/api/products/%s",
monoProduct.block().getId()))))
.body(monoProduct), ProductResponse.class);
}
How can I perform such a task without breaking the reactive principles?
You don't really need to block. You need to build reactive flow combining different operators.
In your case it could look like
public Mono<ServerResponse> handleRequest(ServerRequest serverRequest) {
return getProduct() // Service call to get the Mono<Product>
.map(product -> mapToResponse(product)) // Product -> ProductResponse
.flatMap(response ->
ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.location(URI.create(String.format("/api/products/%s", response.getId())))
.body(BodyInserters.fromValue(response))
);
}

How to get raw token from ReactiveSecurityContextHolder?

I have a method:
#GetMapping("/foo")
public void> foo(JwtAuthenticationToken token) throws ExecutionException, InterruptedException {
Object object = ReactiveSecurityContextHolder.getContext()
.map(securityContext -> securityContext.getAuthentication().getPrincipal())
.toFuture()
.get();
System.out.println(object);
JwtAuthenticationToken object which is method argument is succesfully autowired and not null but
result of
Object object = ReactiveSecurityContextHolder.getContext()
.map(securityContext -> securityContext.getAuthentication().getPrincipal())
.toFuture()
.get();
is null.
Could you please explain why ? Is there way to fix it ?
related topic: How to get jwt token value in spring webflux? (to exchange it with Minio STS token)
It is not necessary to use ReactiveSecurityContextHolder to get the Jwt instance. For example, if JwtAuthenticationToken is non-null, you can get the Jwt instance by doing:
public Mono<Void> foo(JwtAuthenticationToken token) throws ExecutionException, InterruptedException {
Jwt jwt = token.getToken();
// ...
}
Or, you can translate it with #AuthenticationPrincipal like so:
public Mono<Void> foo(#AuthenticationPrincipal Jwt jwt) throws ExecutionException, InterruptedException {
// ...
}
Further, it is not possible to use block() with ReactiveSecurityContextHolder in the way the OP describes. block() subscribes immediately, and there is nothing immediately available in the Reactor Context at this point.
The reactive stack works somewhat in the inverse to perhaps what you are thinking. While there is a filter in Spring Security called ReactorContextWebFilter that populates the Reactor Context with the SecurityContext, its work is deferred until the HTTP response is subscribed to, for example by the browser. block() at this point states (correctly) that the Reactor Context is empty. Instead, if you participate in the existing subscription (instead of calling block()), then you are also deferring your work in the same way and will be able to use ReactiveSecurityContextHolder.
EDIT: Having read the additional context about the OP's situation from How to get jwt token value in spring webflux? (to exchange it with Minio STS token), it's clear now that the OP is aware of these ways to get the instance of a Jwt, and does not want to use them. I'll leave the answer here for completeness anyway.
Not really sure how token is related to the result of the bucketExists and why do you convert it to the CompletableFuture but here is an example how you can get token from the context.
#GetMapping(value = "/someEndpoint")
public Mono<Boolean> foo() {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal())
.cast(Jwt.class)
.map(jwt -> useToken(jwt));
}
Here is a test using org.springframework.security:spring-security-test
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureWebTestClient
class RouteConfigTest {
#Autowired
private WebTestClient client;
#Test
void test() {
this.client.mutateWith(mockJwt())
.get()
.uri("/someEndpoint")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk();
}
}

Does mono.cache works for Webclient response?

I'm trying to cache an authentication token response which is returned by a webclient call.
public Mono<Token> getToken() {
return webclient.post().retrieve()
.bodyToMono(Token.class)
.cache(this::getTokenLiveDuration, t -> Duration.ZERO, () -> Duration.ZERO).log();
}
public Mono<MyResponse> execute() {
return getToken().flatMap(token -> {
webclient.post().header("Auth", token.getValue())
.retrieve()
.bodyToMono(MyResponse.class)
}
}
But if I run execute() two time in a same instance, they have different tokens, which means the cache did not work.
What's the correct way of using .cache or the correct way to cache webclient response?
That's because each time getToken is called a new Mono is created with its own cache.
One way to make caching effective is creating a field for the cached token Mono and use that Mono in the execute method.

How to make reactive #WebFilter work properly with #RequestBody?

I am trying to make a reactive #WebFilter that executes stuff before and after the actual server exchange (i. e. the controller code handling the request):
public static class Foobar implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return Mono.empty()
.doOnEach(s -> /* BEFORE */))
.then(chain.filter(exchange) /* CONTROLLER */)
.and(Mono.empty().doOnEach(s -> /* AFTER */))
.and(Mono.empty().doFinally(s -> /* FINALLY */));
}
}
Everything works as expected for simple GET requests that return a Mono:
#RestController
#RequestMapping
public static class Foo {
#GetMapping
#PostMapping(value = "foo")
public Mono<String> foo(ServerWebExchange exchange) {
return Mono.just("FOOBAR").map(e -> "OK");
}
}
But something really unexpected happens when the controller receives a parameter annotated as #RequestBody. Say, for example a POST request that takes a Mono<String> from the client:
#RestController
#RequestMapping
public static class Bar {
#PostMapping(value = "bar")
public Mono<String> bar(ServerWebExchange exchange, #RequestBody Mono<String> text) {
return text.map(s -> "OK");
}
}
In this case, all steps in my filter are executed before the controller gets to complete the request. This means that the web exchange is committed independently of the filter and therefore I cannot do anything right after the response is sent back to the client.
So I'm wondering:
Is this some kind of Spring bug?
Am I doing something wrong?
Or is this simply the expected behavior?
I've created a small Gist containing a test case that reproduces the problem:
https://gist.github.com/guillermocalvo/740b4fcab471ebc6fe69227fee6d79d5
Edit after Brian's comment:
I still think this might be a bug because Mono.then doesn't seem to have any effect at all:
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.doOnSubscribe(s -> logger.info("onSubscribe response committed:" +
exchange.getResponse().isCommitted()))
.then().doOnEach(s -> logger.info("then doOnEach response committed:" +
exchange.getResponse().isCommitted()))
.doFinally(s -> logger.info("doFinally response committed:" +
exchange.getResponse().isCommitted()));
}
Additionally, if I put stuff in doOnEach is not executed either:
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.doOnSubscribe(s -> logger.info("FILTER-BEFORE-CHAIN/commited=" +
response.isCommitted()))
.doOnEach(s -> logger.info("FILTER-AFTER-CHAIN/commited=" +
response.isCommitted()))
.doFinally(s -> logger.info("FILTER-FINALLY/commited=" +
response.isCommitted()));
}
I don't think this is a bug in Spring (nor in Reactor in this case), but rather a wrong choice of operators to achieve what you're trying to do.
Mono.doOnEach is executed for each signal (next element, completion, error, cancel); this will be executed several times per request in our case.
Mono.and joins the termination signals - so it waits for both Mono to be done and then completes. But both Monos are not executed sequentially, they're subscribed at the same time. Mono.just completes right away, independently of what happens with the filter chain.
In your case, you don't need to have something more complex than adding one operator when the processing starts (doOnSubscribe happens when the mapping has been done and we're starting to execute the handler) and another one when we're done (doFinally happens when it's done, with completion, error or cancellation).
#Component
public class MyFilter implements WebFilter {
Logger logger = LoggerFactory.getLogger(getClass());
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.doOnSubscribe(s -> logger.info("onSubscribe response committed:" +
exchange.getResponse().isCommitted()))
.doFinally(s -> logger.info("doFinally response committed:" +
exchange.getResponse().isCommitted()));
}
}
Note that this approach works as long as you're not performing blocking operations in the DoOnXYZ methods, as they're side effects methods. Doing so will create significant performance problems in your application (and reactor might even reject that operation with an exception). So logging, adding an element to a map are fine - but publishing something to an event queue, for example, is not. In this case, you should use instead chain operations with Mono.then().
Edit
I don't think there is a bug with Mono.then() - doOnEach only works with signals sent downstream (next, error, complete). In this case, you should only get the complete signal. If you'd like to get the context in all cases, you can take a look at Mono.deferWithContext.

How to do Exception Handling for WebFlux using Springboot?

I have 3 micro-service applications. I am trying to do 2 get call async using webclient from reactive package and then combine them whenever I get a response.
Sample code for that:
(referred from - https://docs.spring.io/spring/docs/5.1.9.RELEASE/spring-framework-reference/web-reactive.html#webflux-client-synchronous)
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Person.class);
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlux(Hobby.class).collectList();
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
Map<String, String> map = new LinkedHashMap<>();
map.put("person", personName);
map.put("hobbies", hobbies);
return map;
})
.block();
My question is how can I add exception handling to the get calls?
How do I check if I got a 404 or 204 or something else?
I have tried:
Adding .onStatus() to the GET calls
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new Data4xxException(String.format(
"Could not GET data with id: %s from another app, due to error:
%s", key, clientResponse))))
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new Data5xxException(
String.format("For Data %s, Error Occurred: %s", key, clientResponse))))
Adding exceptionhandlers - but I exactly dont have a controller so this does not seem to be working.
#ExceptionHandler(WebClientException.class)
public Exception handlerWebClientException(WebClientException webClientException) {
return new Data4xxException("Testing", webClientException);
}
Added a class with ControllerAdvice and ExceptionHandler within it
#ControllerAdvice
public class WebFluxExceptionHandler {
#ExceptionHandler(WebClientException.class)
public Exception handlerWebClientException(WebClientException webClientException) {
return new Data4xxException("Testing", webClientException);
}
}
But I don't see them printed in the spring-boot logs.
The Mono.zip.block() method just returns null and does not actually throw any exception.
How do I get the zip method to throw the exception and not return null ?
The way to do it is using onErrorMap in the following way:
Mono<Person> personMono = client.get()
.uri("/person/{id}", personId)
.retrieve()
.bodyToMono(Person.class)
.onErrorMap((Throwable error) -> error);
onErrorMap will make the Mono to actually throw an error when Zip blocks, terminating zip and letting spring or any other class that you want to handle the exception.
You aren't very clear when you ask
"How do I get the zip method to throw the exception and not return null?"
In WebFlux you commonly don't throw exceptions, you propagate exceptions and then handle them. Why? because we are dealing with streams of data, if you throw an exception, the stream ends, client disconnects and the event chain stops.
We still want to maintain the stream of data and handle bad data as it flows through.
You can handle errors using the doOnError method.
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new Data4xxException(String.format(
"Could not GET data with id: %s from another app, due to error:
%s", key, clientResponse))))
Mono.zip( .. ).doOnError( //Handle your error, log or whatever )
If you want to do something more specific you'll have to update your question with how you want your errors to be handled.
The retrieve() method in WebClient throws a WebClientResponseException
whenever a response with status code 4xx or 5xx is received.
Unlike the retrieve() method, the exchange() method does not throw exceptions in the case of 4xx or 5xx responses. You need to check the status codes yourself and handle them in the way you want to.
Mono<Object> result = webClient.get().uri(URL).exchange().log().flatMap(entity -> {
HttpStatus statusCode = entity.statusCode();
if (statusCode.is4xxClientError() || statusCode.is5xxServerError())
{
return Mono.error(new Exception(statusCode.toString()));
}
return Mono.just(entity);
}).flatMap(clientResponse -> clientResponse.bodyToMono(JSONObject.class))
Reference: https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/

Categories

Resources