Description
Summary
This request is to improve the documentation around the behavior of @RestControllerAdvice
and @ControllerAdvice
when both are used in a Spring Boot application. Many developers reasonably expect that:
@RestControllerAdvice
handles exceptions from@RestController
@ControllerAdvice
handles exceptions from@Controller
(typically MVC controllers returning views)
However, both annotations are functionally equivalent in exception resolution unless explicitly scoped. This causes unexpected results when both advice classes are defined globally.
Actual Behavior
When both @ControllerAdvice
and @RestControllerAdvice
are defined without scoping, Spring does not distinguish between which type of controller threw the exception. The resolution order depends on internal registration or @Order
, meaning:
@ControllerAdvice
might end up handling exceptions thrown by a@RestController
, even though@RestControllerAdvice
exists.@RestControllerAdvice
might apply to MVC controllers, returning JSON when a view is expected.
This is non-intuitive and can cause confusing results, especially in mixed applications with both REST and MVC endpoints.
Technical Reason
@RestControllerAdvice
is simply a specialization of @ControllerAdvice
with @ResponseBody
added:
@Target(...)
@Retention(...)
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {}
Therefore, both advice classes are global unless explicitly scoped.
Suggested Documentation Improvements
The documentation should include:
- A clear explanation that both annotations apply globally by default.
- A note that Spring does not route exceptions based on the type of controller (REST vs MVC).
- A recommendation to use the
annotations
attribute of@ControllerAdvice
to explicitly scope handlers to the appropriate controller type.
Recommended Solution (Code Example)
This example shows how to correctly separate exception handling for REST and MVC controllers.
1. REST Controller
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/fail")
public String fail() {
throw new RuntimeException("REST failure");
}
}
2. MVC Controller
@Controller
public class WebController {
@GetMapping("/page")
public String page() {
throw new RuntimeException("MVC failure");
}
}
3. REST Exception Handler
@RestControllerAdvice(annotations = RestController.class)
public class GlobalRestExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleRest(Exception ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Handled in REST handler: " + ex.getMessage());
}
}
4. MVC Exception Handler
@ControllerAdvice(annotations = Controller.class)
public class GlobalMvcExceptionHandler {
@ExceptionHandler(Exception.class)
public ModelAndView handleMvc(Exception ex) {
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "Handled in MVC handler: " + ex.getMessage());
return mv;
}
}
Expected Output
-
GET /api/fail
returns:HTTP/1.1 500 Content-Type: text/plain Handled in REST handler: REST failure
-
GET /page
renderserror.html
with message:Handled in MVC handler: MVC failure
Why This Matters
This undocumented behavior can lead to hours of debugging for developers who assume the framework will "do the right thing" based on controller types. Proper scoping is easy once known — but right now, it is not obvious or documented in the main Spring Boot or Spring Framework guides.
Clarifying this would save developers significant confusion and help ensure exception handling works predictably in mixed REST/MVC applications.