Skip to content

Clarify what @RestControllerAdvice vs @ControllerAdvice apply to by default #34866

Open
@Mamun-Al-Babu-Shikder

Description

@Mamun-Al-Babu-Shikder

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:

  1. A clear explanation that both annotations apply globally by default.
  2. A note that Spring does not route exceptions based on the type of controller (REST vs MVC).
  3. 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 renders error.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.

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)type: documentationA documentation task

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions