Skip to content

Commit 263811e

Browse files
committed
Add WebFlux equivalent of ResponseEntityExceptionHandler
Closes gh-28665
1 parent dfae4ee commit 263811e

File tree

5 files changed

+642
-69
lines changed

5 files changed

+642
-69
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
/*
2+
* Copyright 2002-2022 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.web.reactive.result.method.annotation;
18+
19+
import org.apache.commons.logging.Log;
20+
import org.apache.commons.logging.LogFactory;
21+
import reactor.core.publisher.Mono;
22+
23+
import org.springframework.http.HttpHeaders;
24+
import org.springframework.http.HttpStatusCode;
25+
import org.springframework.http.ProblemDetail;
26+
import org.springframework.http.ResponseEntity;
27+
import org.springframework.lang.Nullable;
28+
import org.springframework.web.ErrorResponse;
29+
import org.springframework.web.ErrorResponseException;
30+
import org.springframework.web.bind.annotation.ControllerAdvice;
31+
import org.springframework.web.bind.annotation.ExceptionHandler;
32+
import org.springframework.web.bind.support.WebExchangeBindException;
33+
import org.springframework.web.server.MethodNotAllowedException;
34+
import org.springframework.web.server.MissingRequestValueException;
35+
import org.springframework.web.server.NotAcceptableStatusException;
36+
import org.springframework.web.server.ResponseStatusException;
37+
import org.springframework.web.server.ServerErrorException;
38+
import org.springframework.web.server.ServerWebExchange;
39+
import org.springframework.web.server.ServerWebInputException;
40+
import org.springframework.web.server.UnsatisfiedRequestParameterException;
41+
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
42+
43+
/**
44+
* A class with an {@code @ExceptionHandler} method that handles all Spring
45+
* WebFlux raised exceptions by returning a {@link ResponseEntity} with
46+
* RFC 7807 formatted error details in the body.
47+
*
48+
* <p>Convenient as a base class of an {@link ControllerAdvice @ControllerAdvice}
49+
* for global exception handling in an application. Subclasses can override
50+
* individual methods that handle a specific exception, override
51+
* {@link #handleExceptionInternal} to override common handling of all exceptions,
52+
* or {@link #createResponseEntity} to intercept the final step of creating the
53+
* @link ResponseEntity} from the selected HTTP status code, headers, and body.
54+
*
55+
* @author Rossen Stoyanchev
56+
* @since 6.0
57+
*/
58+
public abstract class ResponseEntityExceptionHandler {
59+
60+
/**
61+
* Common logger for use in subclasses.
62+
*/
63+
protected final Log logger = LogFactory.getLog(getClass());
64+
65+
66+
/**
67+
* Handle all exceptions raised within Spring MVC handling of the request .
68+
* @param ex the exception to handle
69+
* @param exchange the current request-response
70+
*/
71+
@ExceptionHandler({
72+
MethodNotAllowedException.class,
73+
NotAcceptableStatusException.class,
74+
UnsupportedMediaTypeStatusException.class,
75+
MissingRequestValueException.class,
76+
UnsatisfiedRequestParameterException.class,
77+
WebExchangeBindException.class,
78+
ServerWebInputException.class,
79+
ServerErrorException.class,
80+
ResponseStatusException.class,
81+
ErrorResponseException.class
82+
})
83+
public final Mono<ResponseEntity<Object>> handleException(Exception ex, ServerWebExchange exchange) {
84+
HttpHeaders headers = new HttpHeaders();
85+
86+
if (ex instanceof MethodNotAllowedException theEx) {
87+
return handleMethodNotAllowedException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
88+
}
89+
else if (ex instanceof NotAcceptableStatusException theEx) {
90+
return handleNotAcceptableStatusException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
91+
}
92+
else if (ex instanceof UnsupportedMediaTypeStatusException theEx) {
93+
return handleUnsupportedMediaTypeStatusException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
94+
}
95+
else if (ex instanceof MissingRequestValueException theEx) {
96+
return handleMissingRequestValueException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
97+
}
98+
else if (ex instanceof UnsatisfiedRequestParameterException theEx) {
99+
return handleUnsatisfiedRequestParameterException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
100+
}
101+
else if (ex instanceof WebExchangeBindException theEx) {
102+
return handleWebExchangeBindException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
103+
}
104+
else if (ex instanceof ServerWebInputException theEx) {
105+
return handleServerWebInputException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
106+
}
107+
else if (ex instanceof ServerErrorException theEx) {
108+
return handleServerErrorException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
109+
}
110+
else if (ex instanceof ResponseStatusException theEx) {
111+
return handleResponseStatusException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
112+
}
113+
else if (ex instanceof ErrorResponseException theEx) {
114+
return handleErrorResponseException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
115+
}
116+
else {
117+
if (logger.isWarnEnabled()) {
118+
logger.warn("Unexpected exception type: " + ex.getClass().getName());
119+
}
120+
return Mono.error(ex);
121+
}
122+
}
123+
124+
/**
125+
* Customize the handling of {@link MethodNotAllowedException}.
126+
* <p>This method delegates to {@link #handleExceptionInternal}.
127+
* @param ex the exception to handle
128+
* @param headers the headers to use for the response
129+
* @param status the status code to use for the response
130+
* @param exchange the current request and response
131+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
132+
*/
133+
protected Mono<ResponseEntity<Object>> handleMethodNotAllowedException(
134+
MethodNotAllowedException ex, HttpHeaders headers, HttpStatusCode status,
135+
ServerWebExchange exchange) {
136+
137+
return handleExceptionInternal(ex, null, headers, status, exchange);
138+
}
139+
140+
/**
141+
* Customize the handling of {@link NotAcceptableStatusException}.
142+
* <p>This method delegates to {@link #handleExceptionInternal}.
143+
* @param ex the exception to handle
144+
* @param headers the headers to use for the response
145+
* @param status the status code to use for the response
146+
* @param exchange the current request and response
147+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
148+
*/
149+
protected Mono<ResponseEntity<Object>> handleNotAcceptableStatusException(
150+
NotAcceptableStatusException ex, HttpHeaders headers, HttpStatusCode status,
151+
ServerWebExchange exchange) {
152+
153+
return handleExceptionInternal(ex, null, headers, status, exchange);
154+
}
155+
156+
/**
157+
* Customize the handling of {@link UnsupportedMediaTypeStatusException}.
158+
* <p>This method delegates to {@link #handleExceptionInternal}.
159+
* @param ex the exception to handle
160+
* @param headers the headers to use for the response
161+
* @param status the status code to use for the response
162+
* @param exchange the current request and response
163+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
164+
*/
165+
protected Mono<ResponseEntity<Object>> handleUnsupportedMediaTypeStatusException(
166+
UnsupportedMediaTypeStatusException ex, HttpHeaders headers, HttpStatusCode status,
167+
ServerWebExchange exchange) {
168+
169+
return handleExceptionInternal(ex, null, headers, status, exchange);
170+
}
171+
172+
/**
173+
* Customize the handling of {@link MissingRequestValueException}.
174+
* <p>This method delegates to {@link #handleExceptionInternal}.
175+
* @param ex the exception to handle
176+
* @param headers the headers to use for the response
177+
* @param status the status code to use for the response
178+
* @param exchange the current request and response
179+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
180+
*/
181+
protected Mono<ResponseEntity<Object>> handleMissingRequestValueException(
182+
MissingRequestValueException ex, HttpHeaders headers, HttpStatusCode status,
183+
ServerWebExchange exchange) {
184+
185+
return handleExceptionInternal(ex, null, headers, status, exchange);
186+
}
187+
188+
/**
189+
* Customize the handling of {@link UnsatisfiedRequestParameterException}.
190+
* <p>This method delegates to {@link #handleExceptionInternal}.
191+
* @param ex the exception to handle
192+
* @param headers the headers to use for the response
193+
* @param status the status code to use for the response
194+
* @param exchange the current request and response
195+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
196+
*/
197+
protected Mono<ResponseEntity<Object>> handleUnsatisfiedRequestParameterException(
198+
UnsatisfiedRequestParameterException ex, HttpHeaders headers, HttpStatusCode status,
199+
ServerWebExchange exchange) {
200+
201+
return handleExceptionInternal(ex, null, headers, status, exchange);
202+
}
203+
204+
/**
205+
* Customize the handling of {@link WebExchangeBindException}.
206+
* <p>This method delegates to {@link #handleExceptionInternal}.
207+
* @param ex the exception to handle
208+
* @param headers the headers to use for the response
209+
* @param status the status code to use for the response
210+
* @param exchange the current request and response
211+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
212+
*/
213+
protected Mono<ResponseEntity<Object>> handleWebExchangeBindException(
214+
WebExchangeBindException ex, HttpHeaders headers, HttpStatusCode status,
215+
ServerWebExchange exchange) {
216+
217+
return handleExceptionInternal(ex, null, headers, status, exchange);
218+
}
219+
220+
/**
221+
* Customize the handling of {@link ServerWebInputException}.
222+
* <p>This method delegates to {@link #handleExceptionInternal}.
223+
* @param ex the exception to handle
224+
* @param headers the headers to use for the response
225+
* @param status the status code to use for the response
226+
* @param exchange the current request and response
227+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
228+
*/
229+
protected Mono<ResponseEntity<Object>> handleServerWebInputException(
230+
ServerWebInputException ex, HttpHeaders headers, HttpStatusCode status,
231+
ServerWebExchange exchange) {
232+
233+
return handleExceptionInternal(ex, null, headers, status, exchange);
234+
}
235+
236+
/**
237+
* Customize the handling of any {@link ResponseStatusException}.
238+
* <p>This method delegates to {@link #handleExceptionInternal}.
239+
* @param ex the exception to handle
240+
* @param headers the headers to use for the response
241+
* @param status the status code to use for the response
242+
* @param exchange the current request and response
243+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
244+
*/
245+
protected Mono<ResponseEntity<Object>> handleResponseStatusException(
246+
ResponseStatusException ex, HttpHeaders headers, HttpStatusCode status,
247+
ServerWebExchange exchange) {
248+
249+
return handleExceptionInternal(ex, null, headers, status, exchange);
250+
}
251+
252+
/**
253+
* Customize the handling of {@link ServerErrorException}.
254+
* <p>This method delegates to {@link #handleExceptionInternal}.
255+
* @param ex the exception to handle
256+
* @param headers the headers to use for the response
257+
* @param status the status code to use for the response
258+
* @param exchange the current request and response
259+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
260+
*/
261+
protected Mono<ResponseEntity<Object>> handleServerErrorException(
262+
ServerErrorException ex, HttpHeaders headers, HttpStatusCode status,
263+
ServerWebExchange exchange) {
264+
265+
return handleExceptionInternal(ex, null, headers, status, exchange);
266+
}
267+
268+
/**
269+
* Customize the handling of any {@link ErrorResponseException}.
270+
* <p>This method delegates to {@link #handleExceptionInternal}.
271+
* @param ex the exception to handle
272+
* @param headers the headers to use for the response
273+
* @param status the status code to use for the response
274+
* @param exchange the current request and response
275+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
276+
*/
277+
protected Mono<ResponseEntity<Object>> handleErrorResponseException(
278+
ErrorResponseException ex, HttpHeaders headers, HttpStatusCode status,
279+
ServerWebExchange exchange) {
280+
281+
return handleExceptionInternal(ex, null, headers, status, exchange);
282+
}
283+
284+
/**
285+
* Internal handler method that all others in this class delegate to, for
286+
* common handling, and for the creation of a {@link ResponseEntity}.
287+
* <p>The default implementation does the following:
288+
* <ul>
289+
* <li>return {@code null} if response is already committed
290+
* <li>set the {@code "jakarta.servlet.error.exception"} request attribute
291+
* if the response status is 500 (INTERNAL_SERVER_ERROR).
292+
* <li>extract the {@link ErrorResponse#getBody() body} from
293+
* {@link ErrorResponse} exceptions, if the {@code body} is {@code null}.
294+
* </ul>
295+
* @param ex the exception to handle
296+
* @param body the body to use for the response
297+
* @param headers the headers to use for the response
298+
* @param status the status code to use for the response
299+
* @param exchange the current request and response
300+
* @return a {@code Mono} with the {@code ResponseEntity} for the response
301+
*/
302+
protected Mono<ResponseEntity<Object>> handleExceptionInternal(
303+
Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode status,
304+
ServerWebExchange exchange) {
305+
306+
if (exchange.getResponse().isCommitted()) {
307+
return Mono.error(ex);
308+
}
309+
310+
if (body == null && ex instanceof ErrorResponse errorResponse) {
311+
body = errorResponse.getBody();
312+
}
313+
314+
return createResponseEntity(body, headers, status, exchange);
315+
}
316+
317+
/**
318+
* Create the {@link ResponseEntity} to use from the given body, headers,
319+
* and statusCode. Subclasses can override this method to inspect and possibly
320+
* modify the body, headers, or statusCode, e.g. to re-create an instance of
321+
* {@link ProblemDetail} as an extension of {@link ProblemDetail}.
322+
* @param body the body to use for the response
323+
* @param headers the headers to use for the response
324+
* @param status the status code to use for the response
325+
* @param exchange the current request and response
326+
* @return a {@code Mono} with the created {@code ResponseEntity}
327+
* @since 6.0
328+
*/
329+
protected Mono<ResponseEntity<Object>> createResponseEntity(
330+
@Nullable Object body, HttpHeaders headers, HttpStatusCode status, ServerWebExchange exchange) {
331+
332+
return Mono.just(new ResponseEntity<>(body, headers, status));
333+
}
334+
335+
}

0 commit comments

Comments
 (0)