Improve ResponseEntityExceptionHandler

- update handler methods for lower level exceptions to create a mapping
to a ProblemDetail.
- add protected method for creating the ResponseEntity that a subclass
can override to re-create ProblemDetail without overriding the rest
of what handleExceptionInternal does.
- update Javadoc and polishing

See gh-28439
This commit is contained in:
rstoyanchev 2022-07-11 20:05:03 +01:00
parent 87a2652604
commit b64835d2c8
1 changed files with 216 additions and 170 deletions

View File

@ -25,8 +25,8 @@ import org.springframework.beans.TypeMismatchException;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -50,24 +50,16 @@ import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.util.WebUtils; import org.springframework.web.util.WebUtils;
/** /**
* A convenient base class for {@link ControllerAdvice @ControllerAdvice} classes * A class with an {@code @ExceptionHandler} method that handles all Spring MVC
* that wish to provide centralized exception handling across all * raised exceptions by returning a {@link ResponseEntity} with RFC-7807
* {@code @RequestMapping} methods through {@code @ExceptionHandler} methods. * formatted error details in the body.
* *
* <p>This base class provides an {@code @ExceptionHandler} method for handling * <p>Convenient as a base class of an {@link ControllerAdvice @ControllerAdvice}
* internal Spring MVC exceptions. This method returns a {@code ResponseEntity} * for global exception handling in an application. Subclasses can override
* for writing to the response with a {@link HttpMessageConverter message converter}, * individual methods that handle a specific exception, override
* in contrast to * {@link #handleExceptionInternal} to override common handling of all exceptions,
* {@link org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver * or {@link #createResponseEntity} to intercept the final step of creating the
* DefaultHandlerExceptionResolver} which returns a * @link ResponseEntity} from the selected HTTP status code, headers, and body.
* {@link org.springframework.web.servlet.ModelAndView ModelAndView}.
*
* <p>If there is no need to write error content to the response body, or when
* using view resolution (e.g., via {@code ContentNegotiatingViewResolver}),
* then {@code DefaultHandlerExceptionResolver} is good enough.
*
* <p>Note that in order for an {@code @ControllerAdvice} subclass to be
* detected, {@link ExceptionHandlerExceptionResolver} must be configured.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
@ -95,8 +87,8 @@ public abstract class ResponseEntityExceptionHandler {
/** /**
* Provides handling for standard Spring MVC exceptions. * Handle all exceptions raised within Spring MVC handling of the request .
* @param ex the target exception * @param ex the exception to handle
* @param request the current request * @param request the current request
*/ */
@ExceptionHandler({ @ExceptionHandler({
@ -121,61 +113,56 @@ public abstract class ResponseEntityExceptionHandler {
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception { public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
// ErrorResponse exceptions that expose HTTP response details if (ex instanceof HttpRequestMethodNotSupportedException subEx) {
return handleHttpRequestMethodNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
if (ex instanceof ErrorResponse errorEx) { }
if (ex instanceof HttpRequestMethodNotSupportedException subEx) { else if (ex instanceof HttpMediaTypeNotSupportedException subEx) {
return handleHttpRequestMethodNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); return handleHttpMediaTypeNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
} }
else if (ex instanceof HttpMediaTypeNotSupportedException subEx) { else if (ex instanceof HttpMediaTypeNotAcceptableException subEx) {
return handleHttpMediaTypeNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); return handleHttpMediaTypeNotAcceptable(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
} }
else if (ex instanceof HttpMediaTypeNotAcceptableException subEx) { else if (ex instanceof MissingPathVariableException subEx) {
return handleHttpMediaTypeNotAcceptable(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); return handleMissingPathVariable(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
} }
else if (ex instanceof MissingPathVariableException subEx) { else if (ex instanceof MissingServletRequestParameterException subEx) {
return handleMissingPathVariable(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); return handleMissingServletRequestParameter(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
} }
else if (ex instanceof MissingServletRequestParameterException subEx) { else if (ex instanceof MissingServletRequestPartException subEx) {
return handleMissingServletRequestParameter(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); return handleMissingServletRequestPart(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
} }
else if (ex instanceof MissingServletRequestPartException subEx) { else if (ex instanceof ServletRequestBindingException subEx) {
return handleMissingServletRequestPart(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); return handleServletRequestBindingException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
} }
else if (ex instanceof ServletRequestBindingException subEx) { else if (ex instanceof MethodArgumentNotValidException subEx) {
return handleServletRequestBindingException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); return handleMethodArgumentNotValid(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
} }
else if (ex instanceof MethodArgumentNotValidException subEx) { else if (ex instanceof NoHandlerFoundException subEx) {
return handleMethodArgumentNotValid(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); return handleNoHandlerFoundException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
} }
else if (ex instanceof NoHandlerFoundException subEx) { else if (ex instanceof AsyncRequestTimeoutException subEx) {
return handleNoHandlerFoundException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
} }
else if (ex instanceof AsyncRequestTimeoutException subEx) { else if (ex instanceof ErrorResponse errorEx) {
return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); return handleExceptionInternal(ex, null, errorEx.getHeaders(), errorEx.getStatusCode(), request);
}
else {
// Another ErrorResponseException
return handleExceptionInternal(ex, null, errorEx.getHeaders(), errorEx.getStatusCode(), request);
}
} }
// Other, lower level exceptions // Lower level exceptions, and exceptions used symmetrically on client and server
if (ex instanceof ConversionNotSupportedException cnse) { if (ex instanceof ConversionNotSupportedException theEx) {
return handleConversionNotSupported(cnse, headers, HttpStatus.INTERNAL_SERVER_ERROR, request); return handleConversionNotSupported(theEx, headers, HttpStatus.INTERNAL_SERVER_ERROR, request);
} }
else if (ex instanceof TypeMismatchException tme) { else if (ex instanceof TypeMismatchException theEx) {
return handleTypeMismatch(tme, headers, HttpStatus.BAD_REQUEST, request); return handleTypeMismatch(theEx, headers, HttpStatus.BAD_REQUEST, request);
} }
else if (ex instanceof HttpMessageNotReadableException hmnre) { else if (ex instanceof HttpMessageNotReadableException theEx) {
return handleHttpMessageNotReadable(hmnre, headers, HttpStatus.BAD_REQUEST, request); return handleHttpMessageNotReadable(theEx, headers, HttpStatus.BAD_REQUEST, request);
} }
else if (ex instanceof HttpMessageNotWritableException hmnwe) { else if (ex instanceof HttpMessageNotWritableException theEx) {
return handleHttpMessageNotWritable(hmnwe, headers, HttpStatus.INTERNAL_SERVER_ERROR, request); return handleHttpMessageNotWritable(theEx, headers, HttpStatus.INTERNAL_SERVER_ERROR, request);
} }
else if (ex instanceof BindException be) { else if (ex instanceof BindException theEx) {
return handleBindException(be, headers, HttpStatus.BAD_REQUEST, request); return handleBindException(theEx, headers, HttpStatus.BAD_REQUEST, request);
} }
else { else {
// Unknown exception, typically a wrapper with a common MVC exception as cause // Unknown exception, typically a wrapper with a common MVC exception as cause
@ -187,13 +174,14 @@ public abstract class ResponseEntityExceptionHandler {
} }
/** /**
* Customize the response for HttpRequestMethodNotSupportedException. * Customize the handling of {@link HttpRequestMethodNotSupportedException}.
* <p>This method logs a warning, and delegates to {@link #handleExceptionInternal}. * <p>This method logs a warning and delegates to {@link #handleExceptionInternal}.
* @param ex the exception * @param ex the exception to handle
* @param headers the headers to be written to the response * @param headers the headers to use for the response
* @param status the selected response status * @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported( protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
@ -204,13 +192,14 @@ public abstract class ResponseEntityExceptionHandler {
} }
/** /**
* Customize the response for HttpMediaTypeNotSupportedException. * Customize the handling of {@link HttpMediaTypeNotSupportedException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception * @param ex the exception to handle
* @param headers the headers to be written to the response * @param headers the headers to use for the response
* @param status the selected response status * @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported( protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
@ -220,13 +209,14 @@ public abstract class ResponseEntityExceptionHandler {
} }
/** /**
* Customize the response for HttpMediaTypeNotAcceptableException. * Customize the handling of {@link HttpMediaTypeNotAcceptableException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception * @param ex the exception to handle
* @param headers the headers to be written to the response * @param headers the headers to use for the response
* @param status the selected response status * @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable( protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(
@ -236,13 +226,14 @@ public abstract class ResponseEntityExceptionHandler {
} }
/** /**
* Customize the response for MissingPathVariableException. * Customize the handling of {@link MissingPathVariableException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception * @param ex the exception to handle
* @param headers the headers to be written to the response * @param headers the headers to use for the response
* @param status the selected response status * @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
* @since 4.2 * @since 4.2
*/ */
@Nullable @Nullable
@ -253,13 +244,14 @@ public abstract class ResponseEntityExceptionHandler {
} }
/** /**
* Customize the response for MissingServletRequestParameterException. * Customize the handling of {@link MissingServletRequestParameterException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception * @param ex the exception to handle
* @param headers the headers to be written to the response * @param headers the headers to use for the response
* @param status the selected response status * @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleMissingServletRequestParameter( protected ResponseEntity<Object> handleMissingServletRequestParameter(
@ -269,13 +261,14 @@ public abstract class ResponseEntityExceptionHandler {
} }
/** /**
* Customize the response for MissingServletRequestPartException. * Customize the handling of {@link MissingServletRequestPartException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception * @param ex the exception to handle
* @param headers the headers to be written to the response * @param headers the headers to use for the response
* @param status the selected response status * @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleMissingServletRequestPart( protected ResponseEntity<Object> handleMissingServletRequestPart(
@ -285,13 +278,14 @@ public abstract class ResponseEntityExceptionHandler {
} }
/** /**
* Customize the response for ServletRequestBindingException. * Customize the handling of {@link ServletRequestBindingException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception * @param ex the exception to handle
* @param headers the headers to be written to the response * @param headers the headers to use for the response
* @param status the selected response status * @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleServletRequestBindingException( protected ResponseEntity<Object> handleServletRequestBindingException(
@ -301,13 +295,14 @@ public abstract class ResponseEntityExceptionHandler {
} }
/** /**
* Customize the response for MethodArgumentNotValidException. * Customize the handling of {@link MethodArgumentNotValidException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception * @param ex the exception to handle
* @param headers the headers to be written to the response * @param headers the headers to be written to the response
* @param status the selected response status * @param status the selected response status
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleMethodArgumentNotValid( protected ResponseEntity<Object> handleMethodArgumentNotValid(
@ -317,13 +312,14 @@ public abstract class ResponseEntityExceptionHandler {
} }
/** /**
* Customize the response for NoHandlerFoundException. * Customize the handling of {@link NoHandlerFoundException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception * @param ex the exception to handle
* @param headers the headers to be written to the response * @param headers the headers to use for the response
* @param status the selected response status * @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
* @since 4.0 * @since 4.0
*/ */
@Nullable @Nullable
@ -334,136 +330,186 @@ public abstract class ResponseEntityExceptionHandler {
} }
/** /**
* Customize the response for AsyncRequestTimeoutException. * Customize the handling of {@link AsyncRequestTimeoutException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception * @param ex the exception to handle
* @param headers the headers to be written to the response * @param headers the headers to use for the response
* @param status the selected response status * @param status the status code to use for the response
* @param webRequest the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
* @since 4.2.8 * @since 4.2.8
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleAsyncRequestTimeoutException( protected ResponseEntity<Object> handleAsyncRequestTimeoutException(
AsyncRequestTimeoutException ex, HttpHeaders headers, HttpStatusCode status, WebRequest webRequest) { AsyncRequestTimeoutException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return handleExceptionInternal(ex, null, headers, status, webRequest); return handleExceptionInternal(ex, null, headers, status, request);
} }
/** /**
* Customize the response for ConversionNotSupportedException. * Customize the handling of {@link ConversionNotSupportedException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>By default this method creates a {@link ProblemDetail} with the status
* @param ex the exception * and a short detail message, and then delegates to
* @param headers the headers to be written to the response * {@link #handleExceptionInternal}.
* @param status the selected response status * @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleConversionNotSupported( protected ResponseEntity<Object> handleConversionNotSupported(
ConversionNotSupportedException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { ConversionNotSupportedException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return handleExceptionInternal(ex, null, headers, status, request); ProblemDetail body = ProblemDetail.forStatusAndDetail(status,
"Failed to convert '" + ex.getPropertyName() + "' with value: '" + ex.getValue() + "'");
return handleExceptionInternal(ex, body, headers, status, request);
} }
/** /**
* Customize the response for TypeMismatchException. * Customize the handling of {@link TypeMismatchException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>By default this method creates a {@link ProblemDetail} with the status
* @param ex the exception * and a short detail message, and then delegates to
* @param headers the headers to be written to the response * {@link #handleExceptionInternal}.
* @param status the selected response status * @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleTypeMismatch( protected ResponseEntity<Object> handleTypeMismatch(
TypeMismatchException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { TypeMismatchException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return handleExceptionInternal(ex, null, headers, status, request); ProblemDetail body = ProblemDetail.forStatusAndDetail(status,
"Unexpected type for '" + ex.getPropertyName() + "' with value: '" + ex.getValue() + "'");
return handleExceptionInternal(ex, body, headers, status, request);
} }
/** /**
* Customize the response for HttpMessageNotReadableException. * Customize the handling of {@link HttpMessageNotReadableException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>By default this method creates a {@link ProblemDetail} with the status
* @param ex the exception * and a short detail message, and then delegates to
* @param headers the headers to be written to the response * {@link #handleExceptionInternal}.
* @param status the selected response status * @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleHttpMessageNotReadable( protected ResponseEntity<Object> handleHttpMessageNotReadable(
HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return handleExceptionInternal(ex, null, headers, status, request); ProblemDetail body = ProblemDetail.forStatusAndDetail(status, "Failed to read request body");
return handleExceptionInternal(ex, body, headers, status, request);
} }
/** /**
* Customize the response for HttpMessageNotWritableException. * Customize the handling of {@link HttpMessageNotWritableException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>By default this method creates a {@link ProblemDetail} with the status
* @param ex the exception * and a short detail message, and then delegates to
* @param headers the headers to be written to the response * {@link #handleExceptionInternal}.
* @param status the selected response status * @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleHttpMessageNotWritable( protected ResponseEntity<Object> handleHttpMessageNotWritable(
HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return handleExceptionInternal(ex, null, headers, status, request); ProblemDetail body = ProblemDetail.forStatusAndDetail(status, "Failed to write response body");
return handleExceptionInternal(ex, body, headers, status, request);
} }
/** /**
* Customize the response for BindException. * Customize the handling of {@link BindException}.
* <p>This method delegates to {@link #handleExceptionInternal}. * <p>By default this method creates a {@link ProblemDetail} with the status
* @param ex the exception * and a short detail message, and then delegates to
* @param headers the headers to be written to the response * {@link #handleExceptionInternal}.
* @param status the selected response status * @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param request the current request * @param request the current request
* @return {@code ResponseEntity} or {@code null} if response is committed * @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleBindException( protected ResponseEntity<Object> handleBindException(
BindException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { BindException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return handleExceptionInternal(ex, null, headers, status, request); ProblemDetail body = ProblemDetail.forStatusAndDetail(status, "Failed to bind request");
return handleExceptionInternal(ex, body, headers, status, request);
} }
/** /**
* A single place to customize the response body of all exception types. * Internal handler method that all others in this class delegate to, for
* <p>The default implementation sets the {@link WebUtils#ERROR_EXCEPTION_ATTRIBUTE} * common handling, and for the creation of a {@link ResponseEntity}.
* request attribute and creates a {@link ResponseEntity} from the given * <p>The default implementation does the following:
* body, headers, and status. * <ul>
* @param ex the exception * <li>return {@code null} if response is already committed
* @param body the body for the response * <li>set the {@code "jakarta.servlet.error.exception"} request attribute
* @param headers the headers for the response * if the response status is 500 (INTERNAL_SERVER_ERROR).
* @param statusCode the response status * <li>extract the {@link ErrorResponse#getBody() body} from
* @param webRequest the current request * {@link ErrorResponse} exceptions, if the {@code body} is {@code null}.
* @return {@code ResponseEntity} or {@code null} if response is committed * </ul>
* @param ex the exception to handle
* @param body the body to use for the response
* @param headers the headers to use for the response
* @param statusCode the status code to use for the response
* @param request the current request
* @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
*/ */
@Nullable @Nullable
protected ResponseEntity<Object> handleExceptionInternal( protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest webRequest) { Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
if (webRequest instanceof ServletWebRequest servletWebRequest) { if (request instanceof ServletWebRequest servletWebRequest) {
HttpServletResponse response = servletWebRequest.getResponse(); HttpServletResponse response = servletWebRequest.getResponse();
if (response != null && response.isCommitted()) { if (response != null && response.isCommitted()) {
if (logger.isWarnEnabled()) { if (logger.isWarnEnabled()) {
logger.warn("Ignoring exception, response committed. : " + ex); logger.warn("Response already committed. Ignoring: " + ex);
} }
return null; return null;
} }
} }
if (HttpStatus.INTERNAL_SERVER_ERROR.equals(statusCode)) { if (statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR)) {
webRequest.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
} }
if (body == null && ex instanceof ErrorResponse errorResponse) { if (body == null && ex instanceof ErrorResponse errorResponse) {
body = errorResponse.getBody(); body = errorResponse.getBody();
} }
return createResponseEntity(body, headers, statusCode, request);
}
/**
* Create the {@link ResponseEntity} to use from the given body, headers,
* and statusCode. Subclasses can override this method to inspect and possibly
* modify the body, headers, or statusCode, e.g. to re-create an instance of
* {@link ProblemDetail} as an extension of {@link ProblemDetail}.
* @param body the body to use for the response
* @param headers the headers to use for the response
* @param statusCode the status code to use for the response
* @param request the current request
* @return the {@code ResponseEntity} instance to use
* @since 6.0
*/
protected ResponseEntity<Object> createResponseEntity(
@Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
return new ResponseEntity<>(body, headers, statusCode); return new ResponseEntity<>(body, headers, statusCode);
} }