I'm trying to use WebFlux and I'm seeing a behavior I don't quite understand, I suspect this is a bug in WebFlux or possibly Reactor, but I need confirmation.
I've attempted to create a minimally reproducible case that consist of a very simple HandlerFunction that attempts to return an 200 response, but throws an exception during body creation and then attempts to use onErrorResume to instead return a 404 response.
The handler looks like so:
public Mono<ServerResponse> reproduce(ServerRequest request){
return ServerResponse.ok()
.contentType(APPLICATION_JSON)
.body(Mono.fromCallable(this::trigger),String.class)
.onErrorResume(MinimalExampleException.class,e->ServerResponse.notFound().build());
}
I would expect when calling the associated endpoint that I would get a 404 response. Instead what I'm seeing is a 500 response with log messages indicating Spring believes there was an unhandled exception during request processing.
When I breakpoint inside of onErrorResume I see two handlers being registered, the one I register in the method above, as well as one that's being registered by Spring (inside of RouterFunctions.toHttpHandler
) for instances of ResponseStatusException
. Then during processing of the request I see only the second handler (the one registered by Spring) being called, not matching on the exception being thrown and then falling through to the root level handler.
Near as I can tell, Spring is overwriting onErrorResume at the router level preventing the one I registered in the Handler from being called. Is this the expected behavior? Is there a better way to accomplish what I'm attempting?
The framework indeed has a "catch-all" that is invoked before whatever follows .body(...)
can be invoked. This is by design and I think it will be hard to avoid.
I see 3 solutions:
Like so:
return Mono.fromCallable(this::trigger)
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.syncBody(s))
.onErrorResume(MinimalExampleException.class,
e -> ServerResponse.notFound().build());
ResponseStatusException
You could put the onErrorResume
at the same level as the fromCallable()
(inside the body call) to transform the specific error into a ResponseStatusException via an error mono:
Mono.fromCallable(this::trigger)
.onErrorResume(MinimalExampleException.class,
e->Mono.error(new ResponseStatusException(404, "reason message", e)))
You should be able to annotate a method with @ExceptionHandler
to deal with your particular exception.