From 679432ece6871902b9423af95e5c5bb700e8f547 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 25 Feb 2022 15:07:52 +0000 Subject: [PATCH] DefaultHandlerExceptionResolver supports ErrorResponse DefaultHandlerExceptionResolver now supports ErrorResponse exceptions and can map them to HTTP status and headers of the response. This includes not only exceptions from spring-web, but also any other exception that implements ErrorResponse. ResponseEntityExceptionHandler is updated along the same lines, now also handling any ErrorResponseException. It can be used it for RFC 7807 support for Spring MVC's own exceptions. See gh-27052 --- .../ResponseEntityExceptionHandler.java | 369 ++++++++--------- .../DefaultHandlerExceptionResolver.java | 370 ++++++++++-------- .../ResponseEntityExceptionHandlerTests.java | 37 +- .../DefaultHandlerExceptionResolverTests.java | 17 +- 4 files changed, 417 insertions(+), 376 deletions(-) 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