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