Support Content Negotiation with @ExceptionHandler

Prior to this commit, `@ExceptionHandler` annotated controller methods
could be mapped using the exception type declaration as an annotation
attribute, or as a method parameter.
While such methods support a wide variety of method arguments and return
types, it was not possible to declare the same exception type on
different methods (in the same controller/controller advice).

This commit adds a new `produces` attribute on `@ExceptionHandler`; with
that, applications can vary the HTTP response depending on the exception
type and the requested content-type by the client:

```
@ExceptionHandler(produces = "application/json")
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
	return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42));
}

@ExceptionHandler(produces = "text/html")
public String handle(IllegalArgumentException exc, Model model) {
	model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42));
	return "errorView";
}
```

This commit implements support in both Spring MVC and Spring WebFlux.

Closes gh-31936
This commit is contained in:
Brian Clozel
2024-05-20 17:22:30 +02:00
parent 991be14847
commit 4d4b343815
25 changed files with 984 additions and 212 deletions

View File

@@ -7,43 +7,8 @@
`@ExceptionHandler` methods to handle exceptions from controller methods. The following
example includes such a handler method:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@Controller
public class SimpleController {
// ...
@ExceptionHandler // <1>
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
----
<1> Declaring an `@ExceptionHandler`.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@Controller
class SimpleController {
// ...
@ExceptionHandler // <1>
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
----
<1> Declaring an `@ExceptionHandler`.
======
include-code::./SimpleController[indent=0]
The exception can match against a top-level exception being propagated (that is, a direct
@@ -65,6 +30,22 @@ Support for `@ExceptionHandler` methods in Spring WebFlux is provided by the
`HandlerAdapter` for `@RequestMapping` methods. See xref:web/webflux/dispatcher-handler.adoc[`DispatcherHandler`]
for more detail.
[[webflux-ann-exceptionhandler-media]]
== Media Type Mapping
[.small]#xref:web/webmvc/mvc-controller/ann-exceptionhandler.adoc#mvc-ann-exceptionhandler-media[See equivalent in the Servlet stack]#
In addition to exception types, `@ExceptionHandler` methods can also declare producible media types.
This allows to refine error responses depending on the media types requested by HTTP clients, typically in the "Accept" HTTP request header.
Applications can declare producible media types directly on annotations, for the same exception type:
include-code::./MediaTypeController[tag=mediatype,indent=0]
Here, methods handle the same exception type but will not be rejected as duplicates.
Instead, API clients requesting "application/json" will receive a JSON error, and browsers will get an HTML error view.
Each `@ExceptionHandler` annotation can declare several producible media types,
the content negotiation during the error handling phase will decide which content type will be used.
[[webflux-ann-exceptionhandler-args]]

View File

@@ -6,40 +6,12 @@
`@Controller` and xref:web/webmvc/mvc-controller/ann-advice.adoc[@ControllerAdvice] classes can have
`@ExceptionHandler` methods to handle exceptions from controller methods, as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@Controller
public class SimpleController {
// ...
include-code::./SimpleController[indent=0]
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@Controller
class SimpleController {
// ...
@ExceptionHandler
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
----
======
[[mvc-ann-exceptionhandler-exc]]
== Exception Mapping
The exception may match against a top-level exception being propagated (e.g. a direct
`IOException` being thrown) or against a nested cause within a wrapper exception (e.g.
@@ -54,54 +26,13 @@ is used to sort exceptions based on their depth from the thrown exception type.
Alternatively, the annotation declaration may narrow the exception types to match,
as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
// ...
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
----
======
include-code::./ExceptionController[tag=narrow,indent=0]
You can even use a list of specific exception types with a very generic argument signature,
as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
// ...
}
----
include-code::./ExceptionController[tag=general,indent=0]
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: Exception): ResponseEntity<String> {
// ...
}
----
======
[NOTE]
====
@@ -143,6 +74,25 @@ Support for `@ExceptionHandler` methods in Spring MVC is built on the `Dispatche
level, xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[HandlerExceptionResolver] mechanism.
[[mvc-ann-exceptionhandler-media]]
== Media Type Mapping
[.small]#xref:web/webflux/controller/ann-exceptions.adoc#webflux-ann-exceptionhandler-media[See equivalent in the Reactive stack]#
In addition to exception types, `@ExceptionHandler` methods can also declare producible media types.
This allows to refine error responses depending on the media types requested by HTTP clients, typically in the "Accept" HTTP request header.
Applications can declare producible media types directly on annotations, for the same exception type:
include-code::./MediaTypeController[tag=mediatype,indent=0]
Here, methods handle the same exception type but will not be rejected as duplicates.
Instead, API clients requesting "application/json" will receive a JSON error, and browsers will get an HTML error view.
Each `@ExceptionHandler` annotation can declare several producible media types,
the content negotiation during the error handling phase will decide which content type will be used.
[[mvc-ann-exceptionhandler-args]]
== Method Arguments
[.small]#xref:web/webflux/controller/ann-exceptions.adoc#webflux-ann-exceptionhandler-args[See equivalent in the Reactive stack]#