1

How to intercept and handle errors globally in WebFlux when they are being thrown from WebFilter chain?

It is clear how to handle errors thrown from controllers: @ControllerAdvice and @ExceptionHandler help great.

This approach does not work when an exception is thrown from WebFilter components.

In the following configuration GET /first and GET /second responses intentionally induce exceptions thrown. Although @ExceptionHandler methods handleFirst, handleSecond are similar, the handleSecond is never called. I suppose that is because MyWebFilter does not let a ServerWebExchange go to the stage where GlobalErrorHandlers methods could be applied.

Response for GET /first:

 HTTP 500 "hello first"                   // expected
 HTTP 500 "hello first"                   // actual

Response for GET /second:

 HTTP 404 "hello second"                                                         // expected
 HTTP 500 {"path": "/second", "status": 500, "error": "Internal Server Error" }  // actual

@RestController
class MyController {

    @GetMapping("/first")
    String first(){
        throw new FirstException("hello first");
    }
}


@Component
class MyWebFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange swe, WebFilterChain wfc) {
        var path = swe.getRequest().getURI().getPath();
        if (path.contains("second")){
            throw new SecondException("hello second")
        }
    }
}


@ControllerAdvice
class GlobalErrorHandlers {

    @ExceptionHandler(FirstException::class)
    ResponseEntity<String> handleFirst(FirstException ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.message)
    }

    @ExceptionHandler(SecondException::class)
    ResponseEntity<String> handleSecond(SecondException ex) {
       return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.message)
    }
}
diziaq
  • 6,881
  • 16
  • 54
  • 96
  • Does this answer your question? [How to manage exceptions thrown in filters in Spring?](https://stackoverflow.com/questions/34595605/how-to-manage-exceptions-thrown-in-filters-in-spring) – Toerktumlare Jan 27 '22 at 09:14
  • it is like all applications, either you handle it in the filter, or you have a filter that is in the front catching all thrown exceptions. – Toerktumlare Jan 27 '22 at 09:14
  • You may want to look at [WebExceptionHandler](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/server/WebExceptionHandler.html) – Michael McFadyen Jan 27 '22 at 11:40
  • In my experience with Spring MVC, the `@ControllerAdvice` will not handle exceptions thrown from filters, including Spring Security. It was necessary to install a filter 1st in, last out with `try { chain.doFilter(req, resp); } catch ( Throwable ex ) { handle(ex); }`. This can at least log the error which is otherwise lost. Still trying to sort out WebFlux. Does the relationship between the `@ControllerAdvice` and the `WebFilter(s)` change? Seems possible. – Charlie Reitzel Sep 23 '22 at 19:25
  • 1
    @CharlieReitzel As I noticed while digging into this example, all exceptions somehow are handled by Spring default handlers. The question is: can we infiltrate and override them using configuration? Being no expert in Srping MVC, I believe it has something similar to what is in the answer (mix of correct Priority and implementation of a certain interface). – diziaq Sep 24 '22 at 05:49
  • @diziaq Looks right to me. Seems like a direct effect of asynchronous processing requires keeping the context (i.e. no reliance on thread-local storage). I think your point about ordering the filter is important to get the "last word". But that may be less necessary now, if Spring WebFlux will handle exceptions thrown by filters decently in the 1st place. I'll need to test with a tame filter that throws random exceptions and see what happens ... – Charlie Reitzel Sep 25 '22 at 15:50

2 Answers2

2

Three steps are required to get full control over all exceptions thrown from application endpoints handling code:

  1. Implement org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler
  2. Annotate with @ControllerAdvice (or just @Component)
  3. Set @Priority less than 1 to let the custom handler run before the default one (WebFluxResponseStatusExceptionHandler)

The tricky part is where we get an instance implementing ServerResponse.Context for passing to ServerResponse.writeTo(exchange, context). I did not find the final answer, and comments are welcome. In the internal Spring code they always create a new instance of context for each writeTo invocation, although in all cases (I've manged to find) the context instance is immutable. That is why I ended up using the same ResponseContextInstance for all responses. At the moment no problems detected with this approach.


@ControllerAdvice
@Priority(0) /* should go before WebFluxResponseStatusExceptionHandler */
class CustomWebExceptionHandler : ErrorWebExceptionHandler { 

    private val log = logger(CustomWebExceptionHandler::class)

    override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
        log.error("handled ${ex.javaClass.simpleName}", ex)

        val sr = when (ex) {
            is FirstException -> handleFirst(ex) 
            is SecondException -> handleSecond(ex) 
            else -> defaultException(ex)
        }

        return sr.flatMap { it.writeTo(exchange, ResponseContextInstance) }.then()
    }

    private fun handleFirst(ex: FirstException): Mono<ServerResponse> {
        return ServerResponse
                   .status(HttpStatus.INTERNAL_SERVER_ERROR)
                   .bodyValue("first")
    }

    private fun handleSecond(ex: SecondException): Mono<ServerResponse> {
        return ServerResponse.status(HttpStatus.BAD_REQUEST).bodyValue("second")
    }

    private object ResponseContextInstance : ServerResponse.Context {

        val strategies: HandlerStrategies = HandlerStrategies.withDefaults()

        override fun messageWriters(): List<HttpMessageWriter<*>> {
            return strategies.messageWriters()
        }

        override fun viewResolvers(): List<ViewResolver> {
            return strategies.viewResolvers()
        }
    }
}
diziaq
  • 6,881
  • 16
  • 54
  • 96
  • Is that example Kotlin? Just curious. Should be ok to adapt to Java. – Charlie Reitzel Sep 23 '22 at 19:27
  • @CharlieReitzel Yes that's Kotlin. Convertion to Java is straightforward for this simple case; also could be useful to look at autoconversion in IDEA https://blog.mindorks.com/how-to-convert-a-kotlin-source-file-to-a-java-source-file – diziaq Sep 24 '22 at 05:43
  • No worries, I'll look up the Spring classes and play with it. Exception handlers (in the generic sense) _should_ be stateless, so I think you're approach is good. But I'll do some testing as well. – Charlie Reitzel Sep 25 '22 at 15:53
0

I have not tried it on hand, but seems that from spring-webflux 6.0, you don't need to implement ErrorWebExceptionHandler any more to catch an exception from filter chain, e.g. 405 exception. Just implement a normal controller advice is enough.

See this github issue.

Yuefeng Li
  • 315
  • 3
  • 7