diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java index 6252e7cf25..268f01548c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java @@ -16,9 +16,6 @@ package org.springframework.web.servlet.mvc.method.annotation; -import java.util.List; -import java.util.Set; - import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -26,16 +23,15 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.TypeMismatchException; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; 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; @@ -110,48 +106,61 @@ public abstract class ResponseEntityExceptionHandler { MissingServletRequestParameterException.class, MissingServletRequestPartException.class, ServletRequestBindingException.class, + MethodArgumentNotValidException.class, + NoHandlerFoundException.class, + AsyncRequestTimeoutException.class, + ErrorResponseException.class, ConversionNotSupportedException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, - MethodArgumentNotValidException.class, - BindException.class, - NoHandlerFoundException.class, - AsyncRequestTimeoutException.class + BindException.class }) @Nullable public final ResponseEntity handleException(Exception ex, WebRequest request) throws Exception { HttpHeaders headers = new HttpHeaders(); - if (ex instanceof HttpRequestMethodNotSupportedException) { - HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED; - return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request); + // ErrorResponse exceptions that expose HTTP response details + + if (ex instanceof ErrorResponse errorEx) { + if (ex instanceof HttpRequestMethodNotSupportedException subEx) { + return handleHttpRequestMethodNotSupported(subEx, subEx.getHeaders(), subEx.getStatus(), request); + } + else if (ex instanceof HttpMediaTypeNotSupportedException subEx) { + return handleHttpMediaTypeNotSupported(subEx, subEx.getHeaders(), subEx.getStatus(), request); + } + else if (ex instanceof HttpMediaTypeNotAcceptableException subEx) { + return handleHttpMediaTypeNotAcceptable(subEx, subEx.getHeaders(), subEx.getStatus(), request); + } + else if (ex instanceof MissingPathVariableException subEx) { + return handleMissingPathVariable(subEx, subEx.getHeaders(), subEx.getStatus(), request); + } + else if (ex instanceof MissingServletRequestParameterException subEx) { + return handleMissingServletRequestParameter(subEx, subEx.getHeaders(), subEx.getStatus(), request); + } + else if (ex instanceof MissingServletRequestPartException subEx) { + return handleMissingServletRequestPart(subEx, subEx.getHeaders(), subEx.getStatus(), request); + } + else if (ex instanceof ServletRequestBindingException subEx) { + return handleServletRequestBindingException(subEx, subEx.getHeaders(), subEx.getStatus(), request); + } + else if (ex instanceof MethodArgumentNotValidException subEx) { + return handleMethodArgumentNotValid(subEx, subEx.getHeaders(), subEx.getStatus(), request); + } + else if (ex instanceof NoHandlerFoundException subEx) { + return handleNoHandlerFoundException(subEx, subEx.getHeaders(), subEx.getStatus(), request); + } + else if (ex instanceof AsyncRequestTimeoutException subEx) { + return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatus(), request); + } + else { + return handleExceptionInternal(ex, null, errorEx.getHeaders(), errorEx.getStatus(), request); + } } - else if (ex instanceof HttpMediaTypeNotSupportedException) { - HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE; - return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request); - } - else if (ex instanceof HttpMediaTypeNotAcceptableException) { - HttpStatus status = HttpStatus.NOT_ACCEPTABLE; - return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status, request); - } - else if (ex instanceof MissingPathVariableException) { - HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; - return handleMissingPathVariable((MissingPathVariableException) ex, headers, status, request); - } - else if (ex instanceof MissingServletRequestParameterException) { - HttpStatus status = HttpStatus.BAD_REQUEST; - return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers, status, request); - } - else if (ex instanceof MissingServletRequestPartException) { - HttpStatus status = HttpStatus.BAD_REQUEST; - return handleMissingServletRequestPart((MissingServletRequestPartException) ex, headers, status, request); - } - else if (ex instanceof ServletRequestBindingException) { - HttpStatus status = HttpStatus.BAD_REQUEST; - return handleServletRequestBindingException((ServletRequestBindingException) ex, headers, status, request); - } - else if (ex instanceof ConversionNotSupportedException) { + + // Other, lower level exceptions + + if (ex instanceof ConversionNotSupportedException) { HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; return handleConversionNotSupported((ConversionNotSupportedException) ex, headers, status, request); } @@ -167,22 +176,10 @@ public abstract class ResponseEntityExceptionHandler { HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, headers, status, request); } - else if (ex instanceof MethodArgumentNotValidException) { - HttpStatus status = HttpStatus.BAD_REQUEST; - return handleMethodArgumentNotValid((MethodArgumentNotValidException) ex, headers, status, request); - } else if (ex instanceof BindException) { HttpStatus status = HttpStatus.BAD_REQUEST; return handleBindException((BindException) ex, headers, status, request); } - else if (ex instanceof NoHandlerFoundException) { - HttpStatus status = HttpStatus.NOT_FOUND; - return handleNoHandlerFoundException((NoHandlerFoundException) ex, headers, status, request); - } - else if (ex instanceof AsyncRequestTimeoutException) { - HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE; - return handleAsyncRequestTimeoutException((AsyncRequestTimeoutException) ex, headers, status, request); - } else { // Unknown exception, typically a wrapper with a common MVC exception as cause // (since @ExceptionHandler type declarations also match first-level causes): @@ -194,49 +191,34 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for HttpRequestMethodNotSupportedException. - *

This method logs a warning, sets the "Allow" header, and delegates to - * {@link #handleExceptionInternal}. + *

This method logs a warning, and delegates to {@link #handleExceptionInternal}. * @param ex the exception * @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} instance + * @return {@code ResponseEntity} or {@code null} if response is committed */ + @Nullable protected ResponseEntity handleHttpRequestMethodNotSupported( HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { pageNotFoundLogger.warn(ex.getMessage()); - - Set supportedMethods = ex.getSupportedHttpMethods(); - if (!CollectionUtils.isEmpty(supportedMethods)) { - headers.setAllow(supportedMethods); - } return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the response for HttpMediaTypeNotSupportedException. - *

This method sets the "Accept" header and delegates to - * {@link #handleExceptionInternal}. + *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @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} instance + * @return {@code ResponseEntity} or {@code null} if response is committed */ + @Nullable protected ResponseEntity handleHttpMediaTypeNotSupported( HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { - List mediaTypes = ex.getSupportedMediaTypes(); - if (!CollectionUtils.isEmpty(mediaTypes)) { - headers.setAccept(mediaTypes); - if (request instanceof ServletWebRequest servletWebRequest) { - if (HttpMethod.PATCH.equals(servletWebRequest.getHttpMethod())) { - headers.setAcceptPatch(mediaTypes); - } - } - } - return handleExceptionInternal(ex, null, headers, status, request); } @@ -247,8 +229,9 @@ public abstract class ResponseEntityExceptionHandler { * @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} instance + * @return {@code ResponseEntity} or {@code null} if response is committed */ + @Nullable protected ResponseEntity handleHttpMediaTypeNotAcceptable( HttpMediaTypeNotAcceptableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { @@ -262,9 +245,10 @@ public abstract class ResponseEntityExceptionHandler { * @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} instance + * @return {@code ResponseEntity} or {@code null} if response is committed * @since 4.2 */ + @Nullable protected ResponseEntity handleMissingPathVariable( MissingPathVariableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { @@ -278,104 +262,15 @@ public abstract class ResponseEntityExceptionHandler { * @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} instance + * @return {@code ResponseEntity} or {@code null} if response is committed */ + @Nullable protected ResponseEntity handleMissingServletRequestParameter( MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } - /** - * Customize the response for ServletRequestBindingException. - *

This method delegates to {@link #handleExceptionInternal}. - * @param ex the exception - * @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} instance - */ - protected ResponseEntity handleServletRequestBindingException( - ServletRequestBindingException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { - - return handleExceptionInternal(ex, null, headers, status, request); - } - - /** - * Customize the response for ConversionNotSupportedException. - *

This method delegates to {@link #handleExceptionInternal}. - * @param ex the exception - * @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} instance - */ - protected ResponseEntity handleConversionNotSupported( - ConversionNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { - - return handleExceptionInternal(ex, null, headers, status, request); - } - - /** - * Customize the response for TypeMismatchException. - *

This method delegates to {@link #handleExceptionInternal}. - * @param ex the exception - * @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} instance - */ - protected ResponseEntity handleTypeMismatch( - TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { - - return handleExceptionInternal(ex, null, headers, status, request); - } - - /** - * Customize the response for HttpMessageNotReadableException. - *

This method delegates to {@link #handleExceptionInternal}. - * @param ex the exception - * @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} instance - */ - protected ResponseEntity handleHttpMessageNotReadable( - HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { - - return handleExceptionInternal(ex, null, headers, status, request); - } - - /** - * Customize the response for HttpMessageNotWritableException. - *

This method delegates to {@link #handleExceptionInternal}. - * @param ex the exception - * @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} instance - */ - protected ResponseEntity handleHttpMessageNotWritable( - HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { - - return handleExceptionInternal(ex, null, headers, status, request); - } - - /** - * Customize the response for MethodArgumentNotValidException. - *

This method delegates to {@link #handleExceptionInternal}. - * @param ex the exception - * @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} instance - */ - protected ResponseEntity handleMethodArgumentNotValid( - MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { - - return handleExceptionInternal(ex, null, headers, status, request); - } - /** * Customize the response for MissingServletRequestPartException. *

This method delegates to {@link #handleExceptionInternal}. @@ -383,8 +278,9 @@ public abstract class ResponseEntityExceptionHandler { * @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} instance + * @return {@code ResponseEntity} or {@code null} if response is committed */ + @Nullable protected ResponseEntity handleMissingServletRequestPart( MissingServletRequestPartException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { @@ -392,16 +288,33 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for BindException. + * Customize the response for ServletRequestBindingException. *

This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @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} instance + * @return {@code ResponseEntity} or {@code null} if response is committed */ - protected ResponseEntity handleBindException( - BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + @Nullable + protected ResponseEntity handleServletRequestBindingException( + ServletRequestBindingException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, null, headers, status, request); + } + + /** + * Customize the response for MethodArgumentNotValidException. + *

This method delegates to {@link #handleExceptionInternal}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return {@code ResponseEntity} or {@code null} if response is committed + */ + @Nullable + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } @@ -413,9 +326,10 @@ public abstract class ResponseEntityExceptionHandler { * @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} instance + * @return {@code ResponseEntity} or {@code null} if response is committed * @since 4.0 */ + @Nullable protected ResponseEntity handleNoHandlerFoundException( NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { @@ -429,26 +343,96 @@ public abstract class ResponseEntityExceptionHandler { * @param headers the headers to be written to the response * @param status the selected response status * @param webRequest the current request - * @return a {@code ResponseEntity} instance + * @return {@code ResponseEntity} or {@code null} if response is committed * @since 4.2.8 */ @Nullable protected ResponseEntity handleAsyncRequestTimeoutException( AsyncRequestTimeoutException ex, HttpHeaders headers, HttpStatus status, WebRequest webRequest) { - if (webRequest instanceof ServletWebRequest servletWebRequest) { - HttpServletResponse response = servletWebRequest.getResponse(); - if (response != null && response.isCommitted()) { - if (logger.isWarnEnabled()) { - logger.warn("Async request timed out"); - } - return null; - } - } - return handleExceptionInternal(ex, null, headers, status, webRequest); } + /** + * Customize the response for ConversionNotSupportedException. + *

This method delegates to {@link #handleExceptionInternal}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return {@code ResponseEntity} or {@code null} if response is committed + */ + @Nullable + protected ResponseEntity handleConversionNotSupported( + ConversionNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, null, headers, status, request); + } + + /** + * Customize the response for TypeMismatchException. + *

This method delegates to {@link #handleExceptionInternal}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return {@code ResponseEntity} or {@code null} if response is committed + */ + @Nullable + protected ResponseEntity handleTypeMismatch( + TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, null, headers, status, request); + } + + /** + * Customize the response for HttpMessageNotReadableException. + *

This method delegates to {@link #handleExceptionInternal}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return {@code ResponseEntity} or {@code null} if response is committed + */ + @Nullable + protected ResponseEntity handleHttpMessageNotReadable( + HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, null, headers, status, request); + } + + /** + * Customize the response for HttpMessageNotWritableException. + *

This method delegates to {@link #handleExceptionInternal}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return {@code ResponseEntity} or {@code null} if response is committed + */ + @Nullable + protected ResponseEntity handleHttpMessageNotWritable( + HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, null, headers, status, request); + } + + /** + * Customize the response for BindException. + *

This method delegates to {@link #handleExceptionInternal}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return {@code ResponseEntity} or {@code null} if response is committed + */ + @Nullable + protected ResponseEntity handleBindException( + BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, null, headers, status, request); + } + /** * A single place to customize the response body of all exception types. *

The default implementation sets the {@link WebUtils#ERROR_EXCEPTION_ATTRIBUTE} @@ -458,14 +442,31 @@ public abstract class ResponseEntityExceptionHandler { * @param body the body for the response * @param headers the headers for the response * @param status the response status - * @param request the current request + * @param webRequest the current request + * @return {@code ResponseEntity} or {@code null} if response is committed */ + @Nullable protected ResponseEntity handleExceptionInternal( - Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { + Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest webRequest) { + + if (webRequest instanceof ServletWebRequest servletWebRequest) { + HttpServletResponse response = servletWebRequest.getResponse(); + if (response != null && response.isCommitted()) { + if (logger.isWarnEnabled()) { + logger.warn("Ignoring exception, response committed. : " + ex); + } + return null; + } + } if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) { - request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); + webRequest.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); } + + if (body == null && ex instanceof ErrorResponse errorResponse) { + body = errorResponse.getBody(); + } + return new ResponseEntity<>(body, headers, status); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index 6ee73d49ab..aa106fa068 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -17,7 +17,6 @@ package org.springframework.web.servlet.mvc.support; import java.io.IOException; -import java.util.List; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -27,14 +26,14 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.TypeMismatchException; import org.springframework.core.Ordered; -import org.springframework.http.MediaType; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ProblemDetail; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; +import org.springframework.web.ErrorResponse; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -51,6 +50,7 @@ import org.springframework.web.multipart.support.MissingServletRequestPartExcept import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; +import org.springframework.web.util.WebUtils; /** * The default implementation of the {@link org.springframework.web.servlet.HandlerExceptionResolver} @@ -168,35 +168,58 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { try { - if (ex instanceof HttpRequestMethodNotSupportedException) { - return handleHttpRequestMethodNotSupported( - (HttpRequestMethodNotSupportedException) ex, request, response, handler); + // ErrorResponse exceptions that expose HTTP response details + if (ex instanceof ErrorResponse) { + ModelAndView mav = null; + if (ex instanceof HttpRequestMethodNotSupportedException) { + mav = handleHttpRequestMethodNotSupported( + (HttpRequestMethodNotSupportedException) ex, request, response, handler); + } + else if (ex instanceof HttpMediaTypeNotSupportedException) { + mav = handleHttpMediaTypeNotSupported( + (HttpMediaTypeNotSupportedException) ex, request, response, handler); + } + else if (ex instanceof HttpMediaTypeNotAcceptableException) { + mav = handleHttpMediaTypeNotAcceptable( + (HttpMediaTypeNotAcceptableException) ex, request, response, handler); + } + else if (ex instanceof MissingPathVariableException) { + mav = handleMissingPathVariable( + (MissingPathVariableException) ex, request, response, handler); + } + else if (ex instanceof MissingServletRequestParameterException) { + mav = handleMissingServletRequestParameter( + (MissingServletRequestParameterException) ex, request, response, handler); + } + else if (ex instanceof MissingServletRequestPartException) { + mav = handleMissingServletRequestPartException( + (MissingServletRequestPartException) ex, request, response, handler); + } + else if (ex instanceof ServletRequestBindingException) { + mav = handleServletRequestBindingException( + (ServletRequestBindingException) ex, request, response, handler); + } + else if (ex instanceof MethodArgumentNotValidException) { + mav = handleMethodArgumentNotValidException( + (MethodArgumentNotValidException) ex, request, response, handler); + } + else if (ex instanceof NoHandlerFoundException) { + mav = handleNoHandlerFoundException( + (NoHandlerFoundException) ex, request, response, handler); + } + else if (ex instanceof AsyncRequestTimeoutException) { + mav = handleAsyncRequestTimeoutException( + (AsyncRequestTimeoutException) ex, request, response, handler); + } + + if (mav == null) { + return handleErrorResponse((ErrorResponse) ex, request, response, handler); + } } - else if (ex instanceof HttpMediaTypeNotSupportedException) { - return handleHttpMediaTypeNotSupported( - (HttpMediaTypeNotSupportedException) ex, request, response, handler); - } - else if (ex instanceof HttpMediaTypeNotAcceptableException) { - return handleHttpMediaTypeNotAcceptable( - (HttpMediaTypeNotAcceptableException) ex, request, response, handler); - } - else if (ex instanceof MissingPathVariableException) { - return handleMissingPathVariable( - (MissingPathVariableException) ex, request, response, handler); - } - else if (ex instanceof MissingServletRequestParameterException) { - return handleMissingServletRequestParameter( - (MissingServletRequestParameterException) ex, request, response, handler); - } - else if (ex instanceof MissingServletRequestPartException) { - return handleMissingServletRequestPartException( - (MissingServletRequestPartException) ex, request, response, handler); - } - else if (ex instanceof ServletRequestBindingException) { - return handleServletRequestBindingException( - (ServletRequestBindingException) ex, request, response, handler); - } - else if (ex instanceof ConversionNotSupportedException) { + + // Other, lower level exceptions + + if (ex instanceof ConversionNotSupportedException) { return handleConversionNotSupported( (ConversionNotSupportedException) ex, request, response, handler); } @@ -212,35 +235,23 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes return handleHttpMessageNotWritable( (HttpMessageNotWritableException) ex, request, response, handler); } - else if (ex instanceof MethodArgumentNotValidException) { - return handleMethodArgumentNotValidException( - (MethodArgumentNotValidException) ex, request, response, handler); - } else if (ex instanceof BindException) { return handleBindException((BindException) ex, request, response, handler); } - else if (ex instanceof NoHandlerFoundException) { - return handleNoHandlerFoundException( - (NoHandlerFoundException) ex, request, response, handler); - } - else if (ex instanceof AsyncRequestTimeoutException) { - return handleAsyncRequestTimeoutException( - (AsyncRequestTimeoutException) ex, request, response, handler); - } } catch (Exception handlerEx) { if (logger.isWarnEnabled()) { logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx); } } + return null; } /** - * Handle the case where no request handler method was found for the particular HTTP request method. - *

The default implementation logs a warning, sends an HTTP 405 error, sets the "Allow" header, - * and returns an empty {@code ModelAndView}. Alternatively, a fallback view could be chosen, - * or the HttpRequestMethodNotSupportedException could be rethrown as-is. + * Handle the case where no handler was found for the HTTP method. + *

The default implementation returns {@code null} in which case the + * exception is handled in {@link #handleErrorResponse}. * @param ex the HttpRequestMethodNotSupportedException to be handled * @param request current HTTP request * @param response current HTTP response @@ -249,23 +260,19 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes * @return an empty ModelAndView indicating the exception was handled * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} */ + @Nullable protected ModelAndView handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - String[] supportedMethods = ex.getSupportedMethods(); - if (supportedMethods != null) { - response.setHeader("Allow", StringUtils.arrayToDelimitedString(supportedMethods, ", ")); - } - response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, ex.getMessage()); - return new ModelAndView(); + return null; } /** - * Handle the case where no {@linkplain org.springframework.http.converter.HttpMessageConverter message converters} - * were found for the PUT or POSTed content. - *

The default implementation sends an HTTP 415 error, sets the "Accept" header, - * and returns an empty {@code ModelAndView}. Alternatively, a fallback view could - * be chosen, or the HttpMediaTypeNotSupportedException could be rethrown as-is. + * Handle the case where no + * {@linkplain org.springframework.http.converter.HttpMessageConverter message converters} + * were found for PUT or POSTed content. + *

The default implementation returns {@code null} in which case the + * exception is handled in {@link #handleErrorResponse}. * @param ex the HttpMediaTypeNotSupportedException to be handled * @param request current HTTP request * @param response current HTTP response @@ -273,26 +280,19 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes * @return an empty ModelAndView indicating the exception was handled * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} */ + @Nullable protected ModelAndView handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - List mediaTypes = ex.getSupportedMediaTypes(); - if (!CollectionUtils.isEmpty(mediaTypes)) { - response.setHeader("Accept", MediaType.toString(mediaTypes)); - if (request.getMethod().equals("PATCH")) { - response.setHeader("Accept-Patch", MediaType.toString(mediaTypes)); - } - } - response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); - return new ModelAndView(); + return null; } /** - * Handle the case where no {@linkplain org.springframework.http.converter.HttpMessageConverter message converters} + * Handle the case where no + * {@linkplain org.springframework.http.converter.HttpMessageConverter message converters} * were found that were acceptable for the client (expressed via the {@code Accept} header. - *

The default implementation sends an HTTP 406 error and returns an empty {@code ModelAndView}. - * Alternatively, a fallback view could be chosen, or the HttpMediaTypeNotAcceptableException - * could be rethrown as-is. + *

The default implementation returns {@code null} in which case the + * exception is handled in {@link #handleErrorResponse}. * @param ex the HttpMediaTypeNotAcceptableException to be handled * @param request current HTTP request * @param response current HTTP response @@ -300,18 +300,17 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes * @return an empty ModelAndView indicating the exception was handled * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} */ + @Nullable protected ModelAndView handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE); - return new ModelAndView(); + return null; } /** * Handle the case when a declared path variable does not match any extracted URI variable. - *

The default implementation sends an HTTP 500 error, and returns an empty {@code ModelAndView}. - * Alternatively, a fallback view could be chosen, or the MissingPathVariableException - * could be rethrown as-is. + *

The default implementation returns {@code null} in which case the + * exception is handled in {@link #handleErrorResponse}. * @param ex the MissingPathVariableException to be handled * @param request current HTTP request * @param response current HTTP response @@ -320,18 +319,17 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} * @since 4.2 */ + @Nullable protected ModelAndView handleMissingPathVariable(MissingPathVariableException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage()); - return new ModelAndView(); + return null; } /** * Handle the case when a required parameter is missing. - *

The default implementation sends an HTTP 400 error, and returns an empty {@code ModelAndView}. - * Alternatively, a fallback view could be chosen, or the MissingServletRequestParameterException - * could be rethrown as-is. + *

The default implementation returns {@code null} in which case the + * exception is handled in {@link #handleErrorResponse}. * @param ex the MissingServletRequestParameterException to be handled * @param request current HTTP request * @param response current HTTP response @@ -339,17 +337,35 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes * @return an empty ModelAndView indicating the exception was handled * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} */ + @Nullable protected ModelAndView handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); - return new ModelAndView(); + return null; } /** - * Handle the case when an unrecoverable binding exception occurs - e.g. required header, required cookie. - *

The default implementation sends an HTTP 400 error, and returns an empty {@code ModelAndView}. - * Alternatively, a fallback view could be chosen, or the exception could be rethrown as-is. + * Handle the case where an {@linkplain RequestPart @RequestPart}, a {@link MultipartFile}, + * or a {@code jakarta.servlet.http.Part} argument is required but is missing. + *

By default, an HTTP 400 error is sent back to the client. + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler + * @return an empty ModelAndView indicating the exception was handled + * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} + */ + @Nullable + protected ModelAndView handleMissingServletRequestPartException(MissingServletRequestPartException ex, + HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { + + return null; + } + + /** + * Handle the case when an unrecoverable binding exception occurs - e.g. + * required header, required cookie. + *

The default implementation returns {@code null} in which case the + * exception is handled in {@link #handleErrorResponse}. * @param ex the exception to be handled * @param request current HTTP request * @param response current HTTP response @@ -357,10 +373,106 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes * @return an empty ModelAndView indicating the exception was handled * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} */ + @Nullable protected ModelAndView handleServletRequestBindingException(ServletRequestBindingException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); + return null; + } + + /** + * Handle the case where an argument annotated with {@code @Valid} such as + * an {@link RequestBody} or {@link RequestPart} argument fails validation. + *

The default implementation returns {@code null} in which case the + * exception is handled in {@link #handleErrorResponse}. + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler + * @return an empty ModelAndView indicating the exception was handled + * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} + */ + @Nullable + protected ModelAndView handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, + HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { + + return null; + } + + /** + * Handle the case where no handler was found during the dispatch. + *

The default implementation returns {@code null} in which case the + * exception is handled in {@link #handleErrorResponse}. + * @param ex the NoHandlerFoundException to be handled + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler, or {@code null} if none chosen + * at the time of the exception (for example, if multipart resolution failed) + * @return an empty ModelAndView indicating the exception was handled + * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} + * @since 4.0 + */ + @Nullable + protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex, + HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { + + pageNotFoundLogger.warn(ex.getMessage()); + return null; + } + + /** + * Handle the case where an async request timed out. + *

The default implementation returns {@code null} in which case the + * exception is handled in {@link #handleErrorResponse}. + * @param ex the {@link AsyncRequestTimeoutException} to be handled + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler, or {@code null} if none chosen + * at the time of the exception (for example, if multipart resolution failed) + * @return an empty ModelAndView indicating the exception was handled + * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} + * @since 4.2.8 + */ + @Nullable + protected ModelAndView handleAsyncRequestTimeoutException(AsyncRequestTimeoutException ex, + HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { + + return null; + } + + /** + * Handle an {@link ErrorResponse} exception. + *

The default implementation sets status and the headers of the response + * to those obtained from the {@code ErrorResponse}. If available, the + * {@link ProblemDetail#getDetail()} is used as the message for + * {@link HttpServletResponse#sendError(int, String)}. + * @param errorResponse the exception to be handled + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler + * @return an empty ModelAndView indicating the exception was handled + * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} + * @since 6.0 + */ + protected ModelAndView handleErrorResponse(ErrorResponse errorResponse, + HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { + + if (!response.isCommitted()) { + HttpHeaders headers = errorResponse.getHeaders(); + headers.forEach((name, values) -> values.forEach(value -> response.addHeader(name, value))); + + int status = errorResponse.getRawStatusCode(); + String message = errorResponse.getBody().getDetail(); + if (message != null) { + response.sendError(status, message); + } + else { + response.sendError(status); + } + } + else { + logger.warn("Ignoring exception, response committed. : " + errorResponse); + } + return new ModelAndView(); } @@ -442,40 +554,6 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes return new ModelAndView(); } - /** - * Handle the case where an argument annotated with {@code @Valid} such as - * an {@link RequestBody} or {@link RequestPart} argument fails validation. - *

By default, an HTTP 400 error is sent back to the client. - * @param request current HTTP request - * @param response current HTTP response - * @param handler the executed handler - * @return an empty ModelAndView indicating the exception was handled - * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} - */ - protected ModelAndView handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, - HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return new ModelAndView(); - } - - /** - * Handle the case where an {@linkplain RequestPart @RequestPart}, a {@link MultipartFile}, - * or a {@code jakarta.servlet.http.Part} argument is required but is missing. - *

By default, an HTTP 400 error is sent back to the client. - * @param request current HTTP request - * @param response current HTTP response - * @param handler the executed handler - * @return an empty ModelAndView indicating the exception was handled - * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} - */ - protected ModelAndView handleMissingServletRequestPartException(MissingServletRequestPartException ex, - HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - - response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); - return new ModelAndView(); - } - /** * Handle the case where an {@linkplain ModelAttribute @ModelAttribute} method * argument has binding or validation errors and is not followed by another @@ -494,52 +572,6 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes return new ModelAndView(); } - /** - * Handle the case where no handler was found during the dispatch. - *

The default implementation sends an HTTP 404 error and returns an empty - * {@code ModelAndView}. Alternatively, a fallback view could be chosen, - * or the NoHandlerFoundException could be rethrown as-is. - * @param ex the NoHandlerFoundException to be handled - * @param request current HTTP request - * @param response current HTTP response - * @param handler the executed handler, or {@code null} if none chosen - * at the time of the exception (for example, if multipart resolution failed) - * @return an empty ModelAndView indicating the exception was handled - * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} - * @since 4.0 - */ - protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex, - HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - - pageNotFoundLogger.warn(ex.getMessage()); - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return new ModelAndView(); - } - - /** - * Handle the case where an async request timed out. - *

The default implementation sends an HTTP 503 error. - * @param ex the {@link AsyncRequestTimeoutException} to be handled - * @param request current HTTP request - * @param response current HTTP response - * @param handler the executed handler, or {@code null} if none chosen - * at the time of the exception (for example, if multipart resolution failed) - * @return an empty ModelAndView indicating the exception was handled - * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} - * @since 4.2.8 - */ - protected ModelAndView handleAsyncRequestTimeoutException(AsyncRequestTimeoutException ex, - HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - - if (!response.isCommitted()) { - response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - } - else { - logger.warn("Async request timed out"); - } - return new ModelAndView(); - } - /** * Invoked to send a server error. Sets the status to 500 and also sets the * request attribute "jakarta.servlet.error.exception" to the Exception. @@ -547,7 +579,7 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes protected void sendServerError(Exception ex, HttpServletRequest request, HttpServletResponse response) throws IOException { - request.setAttribute("jakarta.servlet.error.exception", ex); + request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index f469e8c852..fad1bab7b6 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -37,6 +38,7 @@ import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.validation.BindException; +import org.springframework.validation.MapBindingResult; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -60,7 +62,6 @@ import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import org.springframework.web.testfixture.servlet.MockServletConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; /** * Test fixture for {@link ResponseEntityExceptionHandler}. @@ -80,20 +81,21 @@ public class ResponseEntityExceptionHandlerTests { private WebRequest request = new ServletWebRequest(this.servletRequest, this.servletResponse); + @SuppressWarnings("unchecked") @Test public void supportsAllDefaultHandlerExceptionResolverExceptionTypes() throws Exception { - Class clazz = ResponseEntityExceptionHandler.class; - Method handleExceptionMethod = clazz.getMethod("handleException", Exception.class, WebRequest.class); - ExceptionHandler annotation = handleExceptionMethod.getAnnotation(ExceptionHandler.class); - List> exceptionTypes = Arrays.asList(annotation.value()); - for (Method method : DefaultHandlerExceptionResolver.class.getDeclaredMethods()) { - Class[] paramTypes = method.getParameterTypes(); - if (method.getName().startsWith("handle") && (paramTypes.length == 4)) { - String name = paramTypes[0].getSimpleName(); - assertThat(exceptionTypes.contains(paramTypes[0])).as("@ExceptionHandler is missing " + name).isTrue(); - } - } + ExceptionHandler annotation = ResponseEntityExceptionHandler.class + .getMethod("handleException", Exception.class, WebRequest.class) + .getAnnotation(ExceptionHandler.class); + + Arrays.stream(DefaultHandlerExceptionResolver.class.getDeclaredMethods()) + .filter(method -> method.getName().startsWith("handle") && (method.getParameterCount() == 4)) + .filter(method -> !method.getName().equals("handleErrorResponse")) + .map(method -> method.getParameterTypes()[0]) + .forEach(exceptionType -> assertThat(annotation.value()) + .as("@ExceptionHandler is missing declaration for " + exceptionType.getName()) + .contains((Class) exceptionType)); } @Test @@ -121,7 +123,7 @@ public class ResponseEntityExceptionHandlerTests { this.request = new ServletWebRequest(this.servletRequest, this.servletResponse); List acceptable = Arrays.asList(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML); - Exception ex = new HttpMediaTypeNotSupportedException(MediaType.APPLICATION_JSON, acceptable); + Exception ex = new HttpMediaTypeNotSupportedException(MediaType.APPLICATION_JSON, acceptable, HttpMethod.PATCH); ResponseEntity responseEntity = testException(ex); assertThat(responseEntity.getHeaders().getAccept()).isEqualTo(acceptable); @@ -180,8 +182,10 @@ public class ResponseEntityExceptionHandlerTests { } @Test - public void methodArgumentNotValid() { - Exception ex = mock(MethodArgumentNotValidException.class); + public void methodArgumentNotValid() throws Exception { + Exception ex = new MethodArgumentNotValidException( + new MethodParameter(getClass().getDeclaredMethod("handle", String.class), 0), + new MapBindingResult(Collections.emptyMap(), "name")); testException(ex); } @@ -328,6 +332,7 @@ public class ResponseEntityExceptionHandlerTests { protected ResponseEntity handleServletRequestBindingException( ServletRequestBindingException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + headers = new HttpHeaders(); headers.set("someHeader", "someHeaderValue"); return handleExceptionInternal(ex, "error content", headers, status, request); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java index ed98ab46ef..f910a54289 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * 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. @@ -26,6 +26,7 @@ import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.TypeMismatchException; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; @@ -48,6 +49,8 @@ import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; /** + * Unit tests for {@link DefaultHandlerExceptionResolver}. + * * @author Arjen Poutsma */ public class DefaultHandlerExceptionResolverTests { @@ -89,8 +92,10 @@ public class DefaultHandlerExceptionResolverTests { @Test public void patchHttpMediaTypeNotSupported() { - HttpMediaTypeNotSupportedException ex = new HttpMediaTypeNotSupportedException(new MediaType("text", "plain"), - Collections.singletonList(new MediaType("application", "pdf"))); + HttpMediaTypeNotSupportedException ex = new HttpMediaTypeNotSupportedException( + new MediaType("text", "plain"), + Collections.singletonList(new MediaType("application", "pdf")), + HttpMethod.PATCH); MockHttpServletRequest request = new MockHttpServletRequest("PATCH", "/"); ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex); assertThat(mav).as("No ModelAndView returned").isNotNull(); @@ -108,8 +113,7 @@ public class DefaultHandlerExceptionResolverTests { assertThat(mav).as("No ModelAndView returned").isNotNull(); assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue(); assertThat(response.getStatus()).as("Invalid status code").isEqualTo(500); - assertThat(response.getErrorMessage()) - .isEqualTo("Required URI template variable 'foo' for method parameter type String is not present"); + assertThat(response.getErrorMessage()).isEqualTo("Required URI variable 'foo' is not present"); } @Test @@ -119,8 +123,7 @@ public class DefaultHandlerExceptionResolverTests { assertThat(mav).as("No ModelAndView returned").isNotNull(); assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue(); assertThat(response.getStatus()).as("Invalid status code").isEqualTo(400); - assertThat(response.getErrorMessage()).isEqualTo( - "Required request parameter 'foo' for method parameter type bar is not present"); + assertThat(response.getErrorMessage()).isEqualTo("Required parameter 'foo' is not present"); } @Test