001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.servlet.mvc.method.annotation;
018
019import java.util.List;
020import java.util.Set;
021
022import javax.servlet.http.HttpServletResponse;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026
027import org.springframework.beans.ConversionNotSupportedException;
028import org.springframework.beans.TypeMismatchException;
029import org.springframework.http.HttpHeaders;
030import org.springframework.http.HttpMethod;
031import org.springframework.http.HttpStatus;
032import org.springframework.http.MediaType;
033import org.springframework.http.ResponseEntity;
034import org.springframework.http.converter.HttpMessageConverter;
035import org.springframework.http.converter.HttpMessageNotReadableException;
036import org.springframework.http.converter.HttpMessageNotWritableException;
037import org.springframework.lang.Nullable;
038import org.springframework.util.CollectionUtils;
039import org.springframework.validation.BindException;
040import org.springframework.web.HttpMediaTypeNotAcceptableException;
041import org.springframework.web.HttpMediaTypeNotSupportedException;
042import org.springframework.web.HttpRequestMethodNotSupportedException;
043import org.springframework.web.bind.MethodArgumentNotValidException;
044import org.springframework.web.bind.MissingPathVariableException;
045import org.springframework.web.bind.MissingServletRequestParameterException;
046import org.springframework.web.bind.ServletRequestBindingException;
047import org.springframework.web.bind.annotation.ControllerAdvice;
048import org.springframework.web.bind.annotation.ExceptionHandler;
049import org.springframework.web.context.request.ServletWebRequest;
050import org.springframework.web.context.request.WebRequest;
051import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
052import org.springframework.web.multipart.support.MissingServletRequestPartException;
053import org.springframework.web.servlet.NoHandlerFoundException;
054import org.springframework.web.util.WebUtils;
055
056/**
057 * A convenient base class for {@link ControllerAdvice @ControllerAdvice} classes
058 * that wish to provide centralized exception handling across all
059 * {@code @RequestMapping} methods through {@code @ExceptionHandler} methods.
060 *
061 * <p>This base class provides an {@code @ExceptionHandler} method for handling
062 * internal Spring MVC exceptions. This method returns a {@code ResponseEntity}
063 * for writing to the response with a {@link HttpMessageConverter message converter},
064 * in contrast to
065 * {@link org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
066 * DefaultHandlerExceptionResolver} which returns a
067 * {@link org.springframework.web.servlet.ModelAndView ModelAndView}.
068 *
069 * <p>If there is no need to write error content to the response body, or when
070 * using view resolution (e.g., via {@code ContentNegotiatingViewResolver}),
071 * then {@code DefaultHandlerExceptionResolver} is good enough.
072 *
073 * <p>Note that in order for an {@code @ControllerAdvice} subclass to be
074 * detected, {@link ExceptionHandlerExceptionResolver} must be configured.
075 *
076 * @author Rossen Stoyanchev
077 * @since 3.2
078 * @see #handleException(Exception, WebRequest)
079 * @see org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
080 */
081public abstract class ResponseEntityExceptionHandler {
082
083        /**
084         * Log category to use when no mapped handler is found for a request.
085         * @see #pageNotFoundLogger
086         */
087        public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";
088
089        /**
090         * Specific logger to use when no mapped handler is found for a request.
091         * @see #PAGE_NOT_FOUND_LOG_CATEGORY
092         */
093        protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY);
094
095        /**
096         * Common logger for use in subclasses.
097         */
098        protected final Log logger = LogFactory.getLog(getClass());
099
100
101        /**
102         * Provides handling for standard Spring MVC exceptions.
103         * @param ex the target exception
104         * @param request the current request
105         */
106        @ExceptionHandler({
107                        HttpRequestMethodNotSupportedException.class,
108                        HttpMediaTypeNotSupportedException.class,
109                        HttpMediaTypeNotAcceptableException.class,
110                        MissingPathVariableException.class,
111                        MissingServletRequestParameterException.class,
112                        ServletRequestBindingException.class,
113                        ConversionNotSupportedException.class,
114                        TypeMismatchException.class,
115                        HttpMessageNotReadableException.class,
116                        HttpMessageNotWritableException.class,
117                        MethodArgumentNotValidException.class,
118                        MissingServletRequestPartException.class,
119                        BindException.class,
120                        NoHandlerFoundException.class,
121                        AsyncRequestTimeoutException.class
122                })
123        @Nullable
124        public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
125                HttpHeaders headers = new HttpHeaders();
126
127                if (ex instanceof HttpRequestMethodNotSupportedException) {
128                        HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
129                        return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
130                }
131                else if (ex instanceof HttpMediaTypeNotSupportedException) {
132                        HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
133                        return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request);
134                }
135                else if (ex instanceof HttpMediaTypeNotAcceptableException) {
136                        HttpStatus status = HttpStatus.NOT_ACCEPTABLE;
137                        return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status, request);
138                }
139                else if (ex instanceof MissingPathVariableException) {
140                        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
141                        return handleMissingPathVariable((MissingPathVariableException) ex, headers, status, request);
142                }
143                else if (ex instanceof MissingServletRequestParameterException) {
144                        HttpStatus status = HttpStatus.BAD_REQUEST;
145                        return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers, status, request);
146                }
147                else if (ex instanceof ServletRequestBindingException) {
148                        HttpStatus status = HttpStatus.BAD_REQUEST;
149                        return handleServletRequestBindingException((ServletRequestBindingException) ex, headers, status, request);
150                }
151                else if (ex instanceof ConversionNotSupportedException) {
152                        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
153                        return handleConversionNotSupported((ConversionNotSupportedException) ex, headers, status, request);
154                }
155                else if (ex instanceof TypeMismatchException) {
156                        HttpStatus status = HttpStatus.BAD_REQUEST;
157                        return handleTypeMismatch((TypeMismatchException) ex, headers, status, request);
158                }
159                else if (ex instanceof HttpMessageNotReadableException) {
160                        HttpStatus status = HttpStatus.BAD_REQUEST;
161                        return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, headers, status, request);
162                }
163                else if (ex instanceof HttpMessageNotWritableException) {
164                        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
165                        return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, headers, status, request);
166                }
167                else if (ex instanceof MethodArgumentNotValidException) {
168                        HttpStatus status = HttpStatus.BAD_REQUEST;
169                        return handleMethodArgumentNotValid((MethodArgumentNotValidException) ex, headers, status, request);
170                }
171                else if (ex instanceof MissingServletRequestPartException) {
172                        HttpStatus status = HttpStatus.BAD_REQUEST;
173                        return handleMissingServletRequestPart((MissingServletRequestPartException) ex, headers, status, request);
174                }
175                else if (ex instanceof BindException) {
176                        HttpStatus status = HttpStatus.BAD_REQUEST;
177                        return handleBindException((BindException) ex, headers, status, request);
178                }
179                else if (ex instanceof NoHandlerFoundException) {
180                        HttpStatus status = HttpStatus.NOT_FOUND;
181                        return handleNoHandlerFoundException((NoHandlerFoundException) ex, headers, status, request);
182                }
183                else if (ex instanceof AsyncRequestTimeoutException) {
184                        HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE;
185                        return handleAsyncRequestTimeoutException((AsyncRequestTimeoutException) ex, headers, status, request);
186                }
187                else {
188                        // Unknown exception, typically a wrapper with a common MVC exception as cause
189                        // (since @ExceptionHandler type declarations also match first-level causes):
190                        // We only deal with top-level MVC exceptions here, so let's rethrow the given
191                        // exception for further processing through the HandlerExceptionResolver chain.
192                        throw ex;
193                }
194        }
195
196        /**
197         * Customize the response for HttpRequestMethodNotSupportedException.
198         * <p>This method logs a warning, sets the "Allow" header, and delegates to
199         * {@link #handleExceptionInternal}.
200         * @param ex the exception
201         * @param headers the headers to be written to the response
202         * @param status the selected response status
203         * @param request the current request
204         * @return a {@code ResponseEntity} instance
205         */
206        protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
207                        HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
208
209                pageNotFoundLogger.warn(ex.getMessage());
210
211                Set<HttpMethod> supportedMethods = ex.getSupportedHttpMethods();
212                if (!CollectionUtils.isEmpty(supportedMethods)) {
213                        headers.setAllow(supportedMethods);
214                }
215                return handleExceptionInternal(ex, null, headers, status, request);
216        }
217
218        /**
219         * Customize the response for HttpMediaTypeNotSupportedException.
220         * <p>This method sets the "Accept" header and delegates to
221         * {@link #handleExceptionInternal}.
222         * @param ex the exception
223         * @param headers the headers to be written to the response
224         * @param status the selected response status
225         * @param request the current request
226         * @return a {@code ResponseEntity} instance
227         */
228        protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
229                        HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
230
231                List<MediaType> mediaTypes = ex.getSupportedMediaTypes();
232                if (!CollectionUtils.isEmpty(mediaTypes)) {
233                        headers.setAccept(mediaTypes);
234                }
235
236                return handleExceptionInternal(ex, null, headers, status, request);
237        }
238
239        /**
240         * Customize the response for HttpMediaTypeNotAcceptableException.
241         * <p>This method delegates to {@link #handleExceptionInternal}.
242         * @param ex the exception
243         * @param headers the headers to be written to the response
244         * @param status the selected response status
245         * @param request the current request
246         * @return a {@code ResponseEntity} instance
247         */
248        protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(
249                        HttpMediaTypeNotAcceptableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
250
251                return handleExceptionInternal(ex, null, headers, status, request);
252        }
253
254        /**
255         * Customize the response for MissingPathVariableException.
256         * <p>This method delegates to {@link #handleExceptionInternal}.
257         * @param ex the exception
258         * @param headers the headers to be written to the response
259         * @param status the selected response status
260         * @param request the current request
261         * @return a {@code ResponseEntity} instance
262         * @since 4.2
263         */
264        protected ResponseEntity<Object> handleMissingPathVariable(
265                        MissingPathVariableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
266
267                return handleExceptionInternal(ex, null, headers, status, request);
268        }
269
270        /**
271         * Customize the response for MissingServletRequestParameterException.
272         * <p>This method delegates to {@link #handleExceptionInternal}.
273         * @param ex the exception
274         * @param headers the headers to be written to the response
275         * @param status the selected response status
276         * @param request the current request
277         * @return a {@code ResponseEntity} instance
278         */
279        protected ResponseEntity<Object> handleMissingServletRequestParameter(
280                        MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
281
282                return handleExceptionInternal(ex, null, headers, status, request);
283        }
284
285        /**
286         * Customize the response for ServletRequestBindingException.
287         * <p>This method delegates to {@link #handleExceptionInternal}.
288         * @param ex the exception
289         * @param headers the headers to be written to the response
290         * @param status the selected response status
291         * @param request the current request
292         * @return a {@code ResponseEntity} instance
293         */
294        protected ResponseEntity<Object> handleServletRequestBindingException(
295                        ServletRequestBindingException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
296
297                return handleExceptionInternal(ex, null, headers, status, request);
298        }
299
300        /**
301         * Customize the response for ConversionNotSupportedException.
302         * <p>This method delegates to {@link #handleExceptionInternal}.
303         * @param ex the exception
304         * @param headers the headers to be written to the response
305         * @param status the selected response status
306         * @param request the current request
307         * @return a {@code ResponseEntity} instance
308         */
309        protected ResponseEntity<Object> handleConversionNotSupported(
310                        ConversionNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
311
312                return handleExceptionInternal(ex, null, headers, status, request);
313        }
314
315        /**
316         * Customize the response for TypeMismatchException.
317         * <p>This method delegates to {@link #handleExceptionInternal}.
318         * @param ex the exception
319         * @param headers the headers to be written to the response
320         * @param status the selected response status
321         * @param request the current request
322         * @return a {@code ResponseEntity} instance
323         */
324        protected ResponseEntity<Object> handleTypeMismatch(
325                        TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
326
327                return handleExceptionInternal(ex, null, headers, status, request);
328        }
329
330        /**
331         * Customize the response for HttpMessageNotReadableException.
332         * <p>This method delegates to {@link #handleExceptionInternal}.
333         * @param ex the exception
334         * @param headers the headers to be written to the response
335         * @param status the selected response status
336         * @param request the current request
337         * @return a {@code ResponseEntity} instance
338         */
339        protected ResponseEntity<Object> handleHttpMessageNotReadable(
340                        HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
341
342                return handleExceptionInternal(ex, null, headers, status, request);
343        }
344
345        /**
346         * Customize the response for HttpMessageNotWritableException.
347         * <p>This method delegates to {@link #handleExceptionInternal}.
348         * @param ex the exception
349         * @param headers the headers to be written to the response
350         * @param status the selected response status
351         * @param request the current request
352         * @return a {@code ResponseEntity} instance
353         */
354        protected ResponseEntity<Object> handleHttpMessageNotWritable(
355                        HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
356
357                return handleExceptionInternal(ex, null, headers, status, request);
358        }
359
360        /**
361         * Customize the response for MethodArgumentNotValidException.
362         * <p>This method delegates to {@link #handleExceptionInternal}.
363         * @param ex the exception
364         * @param headers the headers to be written to the response
365         * @param status the selected response status
366         * @param request the current request
367         * @return a {@code ResponseEntity} instance
368         */
369        protected ResponseEntity<Object> handleMethodArgumentNotValid(
370                        MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
371
372                return handleExceptionInternal(ex, null, headers, status, request);
373        }
374
375        /**
376         * Customize the response for MissingServletRequestPartException.
377         * <p>This method delegates to {@link #handleExceptionInternal}.
378         * @param ex the exception
379         * @param headers the headers to be written to the response
380         * @param status the selected response status
381         * @param request the current request
382         * @return a {@code ResponseEntity} instance
383         */
384        protected ResponseEntity<Object> handleMissingServletRequestPart(
385                        MissingServletRequestPartException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
386
387                return handleExceptionInternal(ex, null, headers, status, request);
388        }
389
390        /**
391         * Customize the response for BindException.
392         * <p>This method delegates to {@link #handleExceptionInternal}.
393         * @param ex the exception
394         * @param headers the headers to be written to the response
395         * @param status the selected response status
396         * @param request the current request
397         * @return a {@code ResponseEntity} instance
398         */
399        protected ResponseEntity<Object> handleBindException(
400                        BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
401
402                return handleExceptionInternal(ex, null, headers, status, request);
403        }
404
405        /**
406         * Customize the response for NoHandlerFoundException.
407         * <p>This method delegates to {@link #handleExceptionInternal}.
408         * @param ex the exception
409         * @param headers the headers to be written to the response
410         * @param status the selected response status
411         * @param request the current request
412         * @return a {@code ResponseEntity} instance
413         * @since 4.0
414         */
415        protected ResponseEntity<Object> handleNoHandlerFoundException(
416                        NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
417
418                return handleExceptionInternal(ex, null, headers, status, request);
419        }
420
421        /**
422         * Customize the response for AsyncRequestTimeoutException.
423         * <p>This method delegates to {@link #handleExceptionInternal}.
424         * @param ex the exception
425         * @param headers the headers to be written to the response
426         * @param status the selected response status
427         * @param webRequest the current request
428         * @return a {@code ResponseEntity} instance
429         * @since 4.2.8
430         */
431        @Nullable
432        protected ResponseEntity<Object> handleAsyncRequestTimeoutException(
433                        AsyncRequestTimeoutException ex, HttpHeaders headers, HttpStatus status, WebRequest webRequest) {
434
435                if (webRequest instanceof ServletWebRequest) {
436                        ServletWebRequest servletWebRequest = (ServletWebRequest) webRequest;
437                        HttpServletResponse response = servletWebRequest.getResponse();
438                        if (response != null && response.isCommitted()) {
439                                if (logger.isWarnEnabled()) {
440                                        logger.warn("Async request timed out");
441                                }
442                                return null;
443                        }
444                }
445
446                return handleExceptionInternal(ex, null, headers, status, webRequest);
447        }
448
449        /**
450         * A single place to customize the response body of all exception types.
451         * <p>The default implementation sets the {@link WebUtils#ERROR_EXCEPTION_ATTRIBUTE}
452         * request attribute and creates a {@link ResponseEntity} from the given
453         * body, headers, and status.
454         * @param ex the exception
455         * @param body the body for the response
456         * @param headers the headers for the response
457         * @param status the response status
458         * @param request the current request
459         */
460        protected ResponseEntity<Object> handleExceptionInternal(
461                        Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
462
463                if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
464                        request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
465                }
466                return new ResponseEntity<>(body, headers, status);
467        }
468
469}