Skip to content

Commit 4d4b343

Browse files
committed
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
1 parent 991be14 commit 4d4b343

File tree

25 files changed

+987
-215
lines changed

25 files changed

+987
-215
lines changed

framework-docs/modules/ROOT/pages/web/webflux/controller/ann-exceptions.adoc

+17-36
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,8 @@
77
`@ExceptionHandler` methods to handle exceptions from controller methods. The following
88
example includes such a handler method:
99

10-
[tabs]
11-
======
12-
Java::
13-
+
14-
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
15-
----
16-
@Controller
17-
public class SimpleController {
18-
19-
// ...
20-
21-
@ExceptionHandler // <1>
22-
public ResponseEntity<String> handle(IOException ex) {
23-
// ...
24-
}
25-
}
26-
----
27-
<1> Declaring an `@ExceptionHandler`.
28-
29-
Kotlin::
30-
+
31-
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
32-
----
33-
@Controller
34-
class SimpleController {
35-
36-
// ...
37-
38-
@ExceptionHandler // <1>
39-
fun handle(ex: IOException): ResponseEntity<String> {
40-
// ...
41-
}
42-
}
43-
----
44-
<1> Declaring an `@ExceptionHandler`.
45-
======
4610

11+
include-code::./SimpleController[indent=0]
4712

4813

4914
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
6530
`HandlerAdapter` for `@RequestMapping` methods. See xref:web/webflux/dispatcher-handler.adoc[`DispatcherHandler`]
6631
for more detail.
6732

33+
[[webflux-ann-exceptionhandler-media]]
34+
== Media Type Mapping
35+
[.small]#xref:web/webmvc/mvc-controller/ann-exceptionhandler.adoc#mvc-ann-exceptionhandler-media[See equivalent in the Servlet stack]#
36+
37+
In addition to exception types, `@ExceptionHandler` methods can also declare producible media types.
38+
This allows to refine error responses depending on the media types requested by HTTP clients, typically in the "Accept" HTTP request header.
39+
40+
Applications can declare producible media types directly on annotations, for the same exception type:
41+
42+
43+
include-code::./MediaTypeController[tag=mediatype,indent=0]
44+
45+
Here, methods handle the same exception type but will not be rejected as duplicates.
46+
Instead, API clients requesting "application/json" will receive a JSON error, and browsers will get an HTML error view.
47+
Each `@ExceptionHandler` annotation can declare several producible media types,
48+
the content negotiation during the error handling phase will decide which content type will be used.
6849

6950

7051
[[webflux-ann-exceptionhandler-args]]

framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-exceptionhandler.adoc

+28-78
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,12 @@
66
`@Controller` and xref:web/webmvc/mvc-controller/ann-advice.adoc[@ControllerAdvice] classes can have
77
`@ExceptionHandler` methods to handle exceptions from controller methods, as the following example shows:
88

9-
[tabs]
10-
======
11-
Java::
12-
+
13-
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
14-
----
15-
@Controller
16-
public class SimpleController {
17-
18-
// ...
19-
20-
@ExceptionHandler
21-
public ResponseEntity<String> handle(IOException ex) {
22-
// ...
23-
}
24-
}
25-
----
26-
27-
Kotlin::
28-
+
29-
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
30-
----
31-
@Controller
32-
class SimpleController {
33-
34-
// ...
35-
36-
@ExceptionHandler
37-
fun handle(ex: IOException): ResponseEntity<String> {
38-
// ...
39-
}
40-
}
41-
----
42-
======
9+
10+
include-code::./SimpleController[indent=0]
11+
12+
13+
[[mvc-ann-exceptionhandler-exc]]
14+
== Exception Mapping
4315

4416
The exception may match against a top-level exception being propagated (e.g. a direct
4517
`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.
5426
Alternatively, the annotation declaration may narrow the exception types to match,
5527
as the following example shows:
5628

57-
[tabs]
58-
======
59-
Java::
60-
+
61-
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
62-
----
63-
@ExceptionHandler({FileSystemException.class, RemoteException.class})
64-
public ResponseEntity<String> handle(IOException ex) {
65-
// ...
66-
}
67-
----
68-
69-
Kotlin::
70-
+
71-
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
72-
----
73-
@ExceptionHandler(FileSystemException::class, RemoteException::class)
74-
fun handle(ex: IOException): ResponseEntity<String> {
75-
// ...
76-
}
77-
----
78-
======
29+
include-code::./ExceptionController[tag=narrow,indent=0]
7930

8031
You can even use a list of specific exception types with a very generic argument signature,
8132
as the following example shows:
8233

83-
[tabs]
84-
======
85-
Java::
86-
+
87-
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
88-
----
89-
@ExceptionHandler({FileSystemException.class, RemoteException.class})
90-
public ResponseEntity<String> handle(Exception ex) {
91-
// ...
92-
}
93-
----
94-
95-
Kotlin::
96-
+
97-
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
98-
----
99-
@ExceptionHandler(FileSystemException::class, RemoteException::class)
100-
fun handle(ex: Exception): ResponseEntity<String> {
101-
// ...
102-
}
103-
----
104-
======
34+
include-code::./ExceptionController[tag=general,indent=0]
35+
10536

10637
[NOTE]
10738
====
@@ -143,6 +74,25 @@ Support for `@ExceptionHandler` methods in Spring MVC is built on the `Dispatche
14374
level, xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[HandlerExceptionResolver] mechanism.
14475

14576

77+
78+
[[mvc-ann-exceptionhandler-media]]
79+
== Media Type Mapping
80+
[.small]#xref:web/webflux/controller/ann-exceptions.adoc#webflux-ann-exceptionhandler-media[See equivalent in the Reactive stack]#
81+
82+
In addition to exception types, `@ExceptionHandler` methods can also declare producible media types.
83+
This allows to refine error responses depending on the media types requested by HTTP clients, typically in the "Accept" HTTP request header.
84+
85+
Applications can declare producible media types directly on annotations, for the same exception type:
86+
87+
88+
include-code::./MediaTypeController[tag=mediatype,indent=0]
89+
90+
Here, methods handle the same exception type but will not be rejected as duplicates.
91+
Instead, API clients requesting "application/json" will receive a JSON error, and browsers will get an HTML error view.
92+
Each `@ExceptionHandler` annotation can declare several producible media types,
93+
the content negotiation during the error handling phase will decide which content type will be used.
94+
95+
14696
[[mvc-ann-exceptionhandler-args]]
14797
== Method Arguments
14898
[.small]#xref:web/webflux/controller/ann-exceptions.adoc#webflux-ann-exceptionhandler-args[See equivalent in the Reactive stack]#
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.docs.web.webflux.controller.webfluxanncontrollerexceptions;
18+
19+
import java.io.IOException;
20+
21+
import org.springframework.http.ResponseEntity;
22+
import org.springframework.stereotype.Controller;
23+
import org.springframework.web.bind.annotation.ExceptionHandler;
24+
25+
@Controller
26+
public class SimpleController {
27+
28+
@ExceptionHandler(IOException.class)
29+
public ResponseEntity<String> handle() {
30+
return ResponseEntity.internalServerError().body("Could not read file storage");
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.docs.web.webflux.controller.webfluxannexceptionhandlermedia;
18+
19+
import org.springframework.http.ResponseEntity;
20+
import org.springframework.stereotype.Controller;
21+
import org.springframework.ui.Model;
22+
import org.springframework.web.bind.annotation.ExceptionHandler;
23+
24+
@Controller
25+
public class MediaTypeController {
26+
27+
// tag::mediatype[]
28+
@ExceptionHandler(produces = "application/json")
29+
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
30+
return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42));
31+
}
32+
33+
@ExceptionHandler(produces = "text/html")
34+
public String handle(IllegalArgumentException exc, Model model) {
35+
model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42));
36+
return "errorView";
37+
}
38+
// end::mediatype[]
39+
40+
static record ErrorMessage(String message, int code) {
41+
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.docs.web.webmvc.mvccontroller.mvcannexceptionhandler;
18+
19+
import java.io.IOException;
20+
21+
import org.springframework.http.ResponseEntity;
22+
import org.springframework.stereotype.Controller;
23+
import org.springframework.web.bind.annotation.ExceptionHandler;
24+
25+
@Controller
26+
public class SimpleController {
27+
28+
@ExceptionHandler(IOException.class)
29+
public ResponseEntity<String> handle() {
30+
return ResponseEntity.internalServerError().body("Could not read file storage");
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.docs.web.webmvc.mvccontroller.mvcannexceptionhandlerexc;
18+
19+
import java.io.IOException;
20+
import java.nio.file.FileSystemException;
21+
import java.rmi.RemoteException;
22+
23+
import org.springframework.http.ResponseEntity;
24+
import org.springframework.stereotype.Controller;
25+
import org.springframework.web.bind.annotation.ExceptionHandler;
26+
27+
@Controller
28+
public class ExceptionController {
29+
30+
// tag::narrow[]
31+
@ExceptionHandler({FileSystemException.class, RemoteException.class})
32+
public ResponseEntity<String> handleIoException(IOException ex) {
33+
return ResponseEntity.internalServerError().body(ex.getMessage());
34+
}
35+
// end::narrow[]
36+
37+
38+
// tag::general[]
39+
@ExceptionHandler({FileSystemException.class, RemoteException.class})
40+
public ResponseEntity<String> handleExceptions(Exception ex) {
41+
return ResponseEntity.internalServerError().body(ex.getMessage());
42+
}
43+
// end::general[]
44+
45+
}

0 commit comments

Comments
 (0)