All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler Maven / Gradle / Ivy

There is a newer version: 6.1.6
Show newest version
/*
 * Copyright 2002-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.servlet.mvc.method.annotation;

import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.lang.Nullable;
import org.springframework.validation.BindException;
import org.springframework.web.ErrorResponse;
import org.springframework.web.ErrorResponseException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.util.WebUtils;

/**
 * A class with an {@code @ExceptionHandler} method that handles all Spring MVC
 * raised exceptions by returning a {@link ResponseEntity} with RFC 7807
 * formatted error details in the body.
 *
 * 

Convenient as a base class of an {@link ControllerAdvice @ControllerAdvice} * for global exception handling in an application. Subclasses can override * individual methods that handle a specific exception, override * {@link #handleExceptionInternal} to override common handling of all exceptions, * or override {@link #createResponseEntity} to intercept the final step of creating * the {@link ResponseEntity} from the selected HTTP status code, headers, and body. * * @author Rossen Stoyanchev * @since 3.2 */ public abstract class ResponseEntityExceptionHandler implements MessageSourceAware { /** * Log category to use when no mapped handler is found for a request. * @see #pageNotFoundLogger */ public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; /** * Specific logger to use when no mapped handler is found for a request. * @see #PAGE_NOT_FOUND_LOG_CATEGORY */ protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); /** * Common logger for use in subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); @Nullable private MessageSource messageSource; @Override public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } /** * Get the {@link MessageSource} that this exception handler uses. * @since 6.0.3 */ @Nullable protected MessageSource getMessageSource() { return this.messageSource; } /** * Handle all exceptions raised within Spring MVC handling of the request. * @param ex the exception to handle * @param request the current request */ @ExceptionHandler({ HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, MissingServletRequestPartException.class, ServletRequestBindingException.class, MethodArgumentNotValidException.class, NoHandlerFoundException.class, AsyncRequestTimeoutException.class, ErrorResponseException.class, ConversionNotSupportedException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, BindException.class }) @Nullable public final ResponseEntity handleException(Exception ex, WebRequest request) throws Exception { if (ex instanceof HttpRequestMethodNotSupportedException subEx) { return handleHttpRequestMethodNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } else if (ex instanceof HttpMediaTypeNotSupportedException subEx) { return handleHttpMediaTypeNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } else if (ex instanceof HttpMediaTypeNotAcceptableException subEx) { return handleHttpMediaTypeNotAcceptable(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } else if (ex instanceof MissingPathVariableException subEx) { return handleMissingPathVariable(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } else if (ex instanceof MissingServletRequestParameterException subEx) { return handleMissingServletRequestParameter(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } else if (ex instanceof MissingServletRequestPartException subEx) { return handleMissingServletRequestPart(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } else if (ex instanceof ServletRequestBindingException subEx) { return handleServletRequestBindingException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } else if (ex instanceof MethodArgumentNotValidException subEx) { return handleMethodArgumentNotValid(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } else if (ex instanceof NoHandlerFoundException subEx) { return handleNoHandlerFoundException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } else if (ex instanceof AsyncRequestTimeoutException subEx) { return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } else if (ex instanceof ErrorResponseException subEx) { return handleErrorResponseException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); } // Lower level exceptions, and exceptions used symmetrically on client and server HttpHeaders headers = new HttpHeaders(); if (ex instanceof ConversionNotSupportedException theEx) { return handleConversionNotSupported(theEx, headers, HttpStatus.INTERNAL_SERVER_ERROR, request); } else if (ex instanceof TypeMismatchException theEx) { return handleTypeMismatch(theEx, headers, HttpStatus.BAD_REQUEST, request); } else if (ex instanceof HttpMessageNotReadableException theEx) { return handleHttpMessageNotReadable(theEx, headers, HttpStatus.BAD_REQUEST, request); } else if (ex instanceof HttpMessageNotWritableException theEx) { return handleHttpMessageNotWritable(theEx, headers, HttpStatus.INTERNAL_SERVER_ERROR, request); } else if (ex instanceof BindException theEx) { return handleBindException(theEx, headers, HttpStatus.BAD_REQUEST, request); } else { // Unknown exception, typically a wrapper with a common MVC exception as cause // (since @ExceptionHandler type declarations also match nested causes): // We only deal with top-level MVC exceptions here, so let's rethrow the given // exception for further processing through the HandlerExceptionResolver chain. throw ex; } } /** * Customize the handling of {@link HttpRequestMethodNotSupportedException}. *

This method logs a warning and delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleHttpRequestMethodNotSupported( HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { pageNotFoundLogger.warn(ex.getMessage()); return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of {@link HttpMediaTypeNotSupportedException}. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleHttpMediaTypeNotSupported( HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of {@link HttpMediaTypeNotAcceptableException}. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleHttpMediaTypeNotAcceptable( HttpMediaTypeNotAcceptableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of {@link MissingPathVariableException}. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed * @since 4.2 */ @Nullable protected ResponseEntity handleMissingPathVariable( MissingPathVariableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of {@link MissingServletRequestParameterException}. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleMissingServletRequestParameter( MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of {@link MissingServletRequestPartException}. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleMissingServletRequestPart( MissingServletRequestPartException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of {@link ServletRequestBindingException}. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleServletRequestBindingException( ServletRequestBindingException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of {@link MethodArgumentNotValidException}. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to be written to the response * @param status the selected response status * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of {@link NoHandlerFoundException}. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed * @since 4.0 */ @Nullable protected ResponseEntity handleNoHandlerFoundException( NoHandlerFoundException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of {@link AsyncRequestTimeoutException}. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed * @since 4.2.8 */ @Nullable protected ResponseEntity handleAsyncRequestTimeoutException( AsyncRequestTimeoutException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of any {@link ErrorResponseException}. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed * @since 6.0 */ @Nullable protected ResponseEntity handleErrorResponseException( ErrorResponseException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the handling of {@link ConversionNotSupportedException}. *

By default this method creates a {@link ProblemDetail} with the status * and a short detail message, and also looks up an override for the detail * via {@link MessageSource}, before delegating to * {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleConversionNotSupported( ConversionNotSupportedException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { Object[] args = {ex.getPropertyName(), ex.getValue()}; String defaultDetail = "Failed to convert '" + args[0] + "' with value: '" + args[1] + "'"; ProblemDetail body = createProblemDetail(ex, status, defaultDetail, null, args, request); return handleExceptionInternal(ex, body, headers, status, request); } /** * Customize the handling of {@link TypeMismatchException}. *

By default this method creates a {@link ProblemDetail} with the status * and a short detail message, and also looks up an override for the detail * via {@link MessageSource}, before delegating to * {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleTypeMismatch( TypeMismatchException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { Object[] args = {ex.getPropertyName(), ex.getValue()}; String defaultDetail = "Failed to convert '" + args[0] + "' with value: '" + args[1] + "'"; String messageCode = ErrorResponse.getDefaultDetailMessageCode(TypeMismatchException.class, null); ProblemDetail body = createProblemDetail(ex, status, defaultDetail, messageCode, args, request); return handleExceptionInternal(ex, body, headers, status, request); } /** * Customize the handling of {@link HttpMessageNotReadableException}. *

By default this method creates a {@link ProblemDetail} with the status * and a short detail message, and also looks up an override for the detail * via {@link MessageSource}, before delegating to * {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleHttpMessageNotReadable( HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { ProblemDetail body = createProblemDetail(ex, status, "Failed to read request", null, null, request); return handleExceptionInternal(ex, body, headers, status, request); } /** * Customize the handling of {@link HttpMessageNotWritableException}. *

By default this method creates a {@link ProblemDetail} with the status * and a short detail message, and also looks up an override for the detail * via {@link MessageSource}, before delegating to * {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleHttpMessageNotWritable( HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { ProblemDetail body = createProblemDetail(ex, status, "Failed to write request", null, null, request); return handleExceptionInternal(ex, body, headers, status, request); } /** * Customize the handling of {@link BindException}. *

By default this method creates a {@link ProblemDetail} with the status * and a short detail message, and then delegates to * {@link #handleExceptionInternal}. * @param ex the exception to handle * @param headers the headers to use for the response * @param status the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed * @deprecated as of 6.0 since {@link org.springframework.web.method.annotation.ModelAttributeMethodProcessor} * now raises the {@link MethodArgumentNotValidException} subclass instead. */ @Nullable @Deprecated(since = "6.0", forRemoval = true) protected ResponseEntity handleBindException( BindException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { ProblemDetail body = ProblemDetail.forStatusAndDetail(status, "Failed to bind request"); return handleExceptionInternal(ex, body, headers, status, request); } /** * Convenience method to create a {@link ProblemDetail} for any exception * that doesn't implement {@link ErrorResponse}, also performing a * {@link MessageSource} lookup for the "detail" field. * @param ex the exception being handled * @param status the status to associate with the exception * @param defaultDetail default value for the "detail" field * @param detailMessageCode the code to use to look up the "detail" field * through a {@code MessageSource}, falling back on * {@link ErrorResponse#getDefaultDetailMessageCode(Class, String)} * @param detailMessageArguments the arguments to go with the detailMessageCode * @param request the current request * @return the created {@code ProblemDetail} instance * @since 6.0 */ protected ProblemDetail createProblemDetail( Exception ex, HttpStatusCode status, String defaultDetail, @Nullable String detailMessageCode, @Nullable Object[] detailMessageArguments, WebRequest request) { ErrorResponse.Builder builder = ErrorResponse.builder(ex, status, defaultDetail); if (detailMessageCode != null) { builder.detailMessageCode(detailMessageCode); } if (detailMessageArguments != null) { builder.detailMessageArguments(detailMessageArguments); } return builder.build().updateAndGetBody(this.messageSource, LocaleContextHolder.getLocale()); } /** * Internal handler method that all others in this class delegate to, for * common handling, and for the creation of a {@link ResponseEntity}. *

The default implementation does the following: *

    *
  • return {@code null} if response is already committed *
  • set the {@code "jakarta.servlet.error.exception"} request attribute * if the response status is 500 (INTERNAL_SERVER_ERROR). *
  • extract the {@link ErrorResponse#getBody() body} from * {@link ErrorResponse} exceptions, if the {@code body} is {@code null}. *
* @param ex the exception to handle * @param body the body to use for the response * @param headers the headers to use for the response * @param statusCode the status code to use for the response * @param request the current request * @return a {@code ResponseEntity} for the response to use, possibly * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleExceptionInternal( Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) { if (request instanceof ServletWebRequest servletWebRequest) { HttpServletResponse response = servletWebRequest.getResponse(); if (response != null && response.isCommitted()) { if (logger.isWarnEnabled()) { logger.warn("Response already committed. Ignoring: " + ex); } return null; } } if (statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR)) { request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); } if (body == null && ex instanceof ErrorResponse errorResponse) { body = errorResponse.updateAndGetBody(this.messageSource, LocaleContextHolder.getLocale()); } return createResponseEntity(body, headers, statusCode, request); } /** * Create the {@link ResponseEntity} to use from the given body, headers, * and statusCode. Subclasses can override this method to inspect and possibly * modify the body, headers, or statusCode, e.g. to re-create an instance of * {@link ProblemDetail} as an extension of {@link ProblemDetail}. * @param body the body to use for the response * @param headers the headers to use for the response * @param statusCode the status code to use for the response * @param request the current request * @return the {@code ResponseEntity} instance to use * @since 6.0 */ protected ResponseEntity createResponseEntity( @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) { return new ResponseEntity<>(body, headers, statusCode); } }