Files
asmhatreandClaude Opus 5 926250e1a9 Add problem-details: global exception handling with RFC 9457
Companion code for "Global Exception Handling with ProblemDetail (RFC 9457)
in Spring Boot 4". Thirteen failures under five handling setups (Boot
defaults, the Boot flag, a ResponseEntityExceptionHandler advice, advice
plus an ErrorController, a catch-all ordered first), validation errors,
i18n, content negotiation, Security's 401/403, silent 500s and decoding
on the client. 16 tests pin the behaviour.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
2026-09-11 17:12:22 +00:00

4.7 KiB

2. Choosing a mechanism

← 1. Mental model · Index · Next: 3. Validation errors →

The same thirteen failures under five setups - matrix-summary.txt, produced by scripts/demo-matrix.sh:

failure                                    | defaults               | boot-flag              | advice                 | advice,errors          | catchall-first        
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
domain exception (OrderNotFoundException)  | 500 Boot /error JSON   | 500 Boot /error JSON   | 404 problem+json       | 404 problem+json       | 500 problem+json      
type mismatch (/orders/abc)                | 400 Boot /error JSON   | 400 problem+json       | 400 problem+json       | 400 problem+json       | 500 problem+json      
exception in a servlet filter              | 500 Boot /error JSON   | 500 Boot /error JSON   | 500 Boot /error JSON   | 500 problem+json       | 500 Boot /error JSON  
401 no credentials                         | 401 Boot /error JSON   | 401 Boot /error JSON   | 401 problem+json       | 401 problem+json       | 401 Boot /error JSON  

spring.mvc.problemdetails.enabled

Defaults to false in Spring Boot 4.1.1 (spring-configuration-metadata.json in spring-boot-webmvc). When true, WebMvcAutoConfiguration$ProblemDetailsErrorHandlingConfiguration registers ProblemDetailsExceptionHandler - an empty subclass of ResponseEntityExceptionHandler - with:

  • @ConditionalOnMissingBean(ResponseEntityExceptionHandler.class) - it backs off as soon as you declare your own subclass (advice-order.txt, profiles boot-flag,advice)
  • @Order(0) - it is consulted before any advice with the default (lowest) precedence

It handles exactly what ResponseEntityExceptionHandler handles: Spring MVC's exceptions and any ErrorResponse. It does not handle your exceptions, which is why the first and tenth rows stay Boot's JSON. Turning the flag on and stopping there gives an API with two error shapes.

Advice ordering, and the catch-all trap

ExceptionHandlerExceptionResolver walks the advices in order and uses the first advice that has any matching handler, not the most specific handler across all advices. The catchall-first profile declares @ExceptionHandler(Exception.class) at Ordered.HIGHEST_PRECEDENCE; it matches NoResourceFoundException, HttpRequestMethodNotSupportedException and every other framework exception before Boot's handler at order 0 gets a look. Every 4xx in the matrix becomes a 500.

Keep the catch-all in the same class as your specific handlers (the most specific handler wins within one class), or give it the lowest precedence.

The handler that stops the application starting

Extending ResponseEntityExceptionHandler and adding @ExceptionHandler(MethodArgumentNotValidException.class) - the obvious way to customise validation errors - fails at startup (ambiguous-handler.txt):

Caused by: java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for [ExceptionHandler{exceptionType=org.springframework.web.bind.MethodArgumentNotValidException, mediaType=*/*}]:
    {org.springframework.http.ProblemDetail com.ankurm.problems.advice.AmbiguousExceptionHandler.invalid(org.springframework.web.bind.MethodArgumentNotValidException),
     public final org.springframework.http.ResponseEntity org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.context.request.WebRequest) throws java.lang.Exception}

The base class's handleException is final and already mapped. Override the protected handleMethodArgumentNotValid(...) instead - chapter 3.

advice + errors:

AdviceContractTest pins it.