From b64835d2c8d6e195aa6d553fda6230b392b6f4f0 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 11 Jul 2022 20:05:03 +0100 Subject: [PATCH] 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 --- .../ResponseEntityExceptionHandler.java | 386 ++++++++++-------- 1 file changed, 216 insertions(+), 170 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 9aac2e3c72..2c4903f540 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 @@ -25,8 +25,8 @@ import org.springframework.beans.TypeMismatchException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; +import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.lang.Nullable; @@ -50,24 +50,16 @@ import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.util.WebUtils; /** - * A convenient base class for {@link ControllerAdvice @ControllerAdvice} classes - * that wish to provide centralized exception handling across all - * {@code @RequestMapping} methods through {@code @ExceptionHandler} methods. + * A class with an {@code @ExceptionHandler} method that handles all Spring MVC + * raised exceptions by returning a {@link ResponseEntity} with RFC-7807 + * formatted error details in the body. * - *

This base class provides an {@code @ExceptionHandler} method for handling - * internal Spring MVC exceptions. This method returns a {@code ResponseEntity} - * for writing to the response with a {@link HttpMessageConverter message converter}, - * in contrast to - * {@link org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver - * DefaultHandlerExceptionResolver} which returns a - * {@link org.springframework.web.servlet.ModelAndView ModelAndView}. - * - *

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. - * - *

Note that in order for an {@code @ControllerAdvice} subclass to be - * detected, {@link ExceptionHandlerExceptionResolver} must be configured. + *

Convenient as a base class of an {@link ControllerAdvice @ControllerAdvice} + * for global exception handling in an application. Subclasses can override + * individual methods that handle a specific exception, override + * {@link #handleExceptionInternal} to override common handling of all exceptions, + * or {@link #createResponseEntity} to intercept the final step of creating the + * @link ResponseEntity} from the selected HTTP status code, headers, and body. * * @author Rossen Stoyanchev * @since 3.2 @@ -95,8 +87,8 @@ public abstract class ResponseEntityExceptionHandler { /** - * Provides handling for standard Spring MVC exceptions. - * @param ex the target exception + * Handle all exceptions raised within Spring MVC handling of the request . + * @param ex the exception to handle * @param request the current request */ @ExceptionHandler({ @@ -121,61 +113,56 @@ public abstract class ResponseEntityExceptionHandler { public final ResponseEntity handleException(Exception ex, WebRequest request) throws Exception { HttpHeaders headers = new HttpHeaders(); - // ErrorResponse exceptions that expose HTTP response details - - if (ex instanceof ErrorResponse errorEx) { - if (ex instanceof HttpRequestMethodNotSupportedException subEx) { - return handleHttpRequestMethodNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); - } - else if (ex instanceof HttpMediaTypeNotSupportedException subEx) { - return handleHttpMediaTypeNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); - } - else if (ex instanceof HttpMediaTypeNotAcceptableException subEx) { - return handleHttpMediaTypeNotAcceptable(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); - } - else if (ex instanceof MissingPathVariableException subEx) { - return handleMissingPathVariable(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); - } - else if (ex instanceof MissingServletRequestParameterException subEx) { - return handleMissingServletRequestParameter(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); - } - else if (ex instanceof MissingServletRequestPartException subEx) { - return handleMissingServletRequestPart(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); - } - else if (ex instanceof ServletRequestBindingException subEx) { - return handleServletRequestBindingException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); - } - else if (ex instanceof MethodArgumentNotValidException subEx) { - return handleMethodArgumentNotValid(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); - } - else if (ex instanceof NoHandlerFoundException subEx) { - return handleNoHandlerFoundException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); - } - else if (ex instanceof AsyncRequestTimeoutException subEx) { - return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); - } - else { - // Another ErrorResponseException - return handleExceptionInternal(ex, null, errorEx.getHeaders(), errorEx.getStatusCode(), request); - } + if (ex instanceof HttpRequestMethodNotSupportedException subEx) { + return handleHttpRequestMethodNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); + } + else if (ex instanceof HttpMediaTypeNotSupportedException subEx) { + return handleHttpMediaTypeNotSupported(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); + } + else if (ex instanceof HttpMediaTypeNotAcceptableException subEx) { + return handleHttpMediaTypeNotAcceptable(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); + } + else if (ex instanceof MissingPathVariableException subEx) { + return handleMissingPathVariable(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); + } + else if (ex instanceof MissingServletRequestParameterException subEx) { + return handleMissingServletRequestParameter(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); + } + else if (ex instanceof MissingServletRequestPartException subEx) { + return handleMissingServletRequestPart(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); + } + else if (ex instanceof ServletRequestBindingException subEx) { + return handleServletRequestBindingException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); + } + else if (ex instanceof MethodArgumentNotValidException subEx) { + return handleMethodArgumentNotValid(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); + } + else if (ex instanceof NoHandlerFoundException subEx) { + return handleNoHandlerFoundException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); + } + else if (ex instanceof AsyncRequestTimeoutException subEx) { + return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request); + } + else if (ex instanceof ErrorResponse errorEx) { + 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) { - return handleConversionNotSupported(cnse, headers, HttpStatus.INTERNAL_SERVER_ERROR, request); + if (ex instanceof ConversionNotSupportedException theEx) { + return handleConversionNotSupported(theEx, headers, HttpStatus.INTERNAL_SERVER_ERROR, request); } - else if (ex instanceof TypeMismatchException tme) { - return handleTypeMismatch(tme, headers, HttpStatus.BAD_REQUEST, request); + else if (ex instanceof TypeMismatchException theEx) { + return handleTypeMismatch(theEx, headers, HttpStatus.BAD_REQUEST, request); } - else if (ex instanceof HttpMessageNotReadableException hmnre) { - return handleHttpMessageNotReadable(hmnre, headers, HttpStatus.BAD_REQUEST, request); + else if (ex instanceof HttpMessageNotReadableException theEx) { + return handleHttpMessageNotReadable(theEx, headers, HttpStatus.BAD_REQUEST, request); } - else if (ex instanceof HttpMessageNotWritableException hmnwe) { - return handleHttpMessageNotWritable(hmnwe, headers, HttpStatus.INTERNAL_SERVER_ERROR, request); + else if (ex instanceof HttpMessageNotWritableException theEx) { + return handleHttpMessageNotWritable(theEx, headers, HttpStatus.INTERNAL_SERVER_ERROR, request); } - else if (ex instanceof BindException be) { - return handleBindException(be, headers, HttpStatus.BAD_REQUEST, request); + else if (ex instanceof BindException theEx) { + return handleBindException(theEx, headers, HttpStatus.BAD_REQUEST, request); } else { // Unknown exception, typically a wrapper with a common MVC exception as cause @@ -187,13 +174,14 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for HttpRequestMethodNotSupportedException. - *

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 + * Customize the handling of {@link HttpRequestMethodNotSupportedException}. + *

This method logs a warning and delegates to {@link #handleExceptionInternal}. + * @param ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleHttpRequestMethodNotSupported( @@ -204,13 +192,14 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for HttpMediaTypeNotSupportedException. + * Customize the handling of {@link HttpMediaTypeNotSupportedException}. *

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 ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleHttpMediaTypeNotSupported( @@ -220,13 +209,14 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for HttpMediaTypeNotAcceptableException. + * Customize the handling of {@link HttpMediaTypeNotAcceptableException}. *

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 ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleHttpMediaTypeNotAcceptable( @@ -236,13 +226,14 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for MissingPathVariableException. + * Customize the handling of {@link MissingPathVariableException}. *

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 ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 */ @Nullable @@ -253,13 +244,14 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for MissingServletRequestParameterException. + * Customize the handling of {@link MissingServletRequestParameterException}. *

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 ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleMissingServletRequestParameter( @@ -269,13 +261,14 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for MissingServletRequestPartException. + * Customize the handling of {@link MissingServletRequestPartException}. *

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 ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleMissingServletRequestPart( @@ -285,13 +278,14 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for ServletRequestBindingException. + * Customize the handling of {@link 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 ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleServletRequestBindingException( @@ -301,13 +295,14 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for MethodArgumentNotValidException. + * Customize the handling of {@link MethodArgumentNotValidException}. *

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 status the selected response status * @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 protected ResponseEntity handleMethodArgumentNotValid( @@ -317,13 +312,14 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for NoHandlerFoundException. + * Customize the handling of {@link NoHandlerFoundException}. *

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 ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 */ @Nullable @@ -334,136 +330,186 @@ public abstract class ResponseEntityExceptionHandler { } /** - * Customize the response for AsyncRequestTimeoutException. + * Customize the handling of {@link AsyncRequestTimeoutException}. *

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 webRequest the current request - * @return {@code ResponseEntity} or {@code null} if response is committed + * @param ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response + * @param request the current request + * @return a {@code ResponseEntity} for the response to use, possibly + * {@code null} when the response is already committed * @since 4.2.8 */ @Nullable protected ResponseEntity handleAsyncRequestTimeoutException( - AsyncRequestTimeoutException ex, HttpHeaders headers, HttpStatusCode status, WebRequest 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. - *

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 + * Customize the handling of {@link ConversionNotSupportedException}. + *

By default this method creates a {@link ProblemDetail} with the status + * and a short detail message, and then delegates to + * {@link #handleExceptionInternal}. + * @param ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleConversionNotSupported( 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. - *

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 + * Customize the handling of {@link TypeMismatchException}. + *

By default this method creates a {@link ProblemDetail} with the status + * and a short detail message, and then delegates to + * {@link #handleExceptionInternal}. + * @param ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleTypeMismatch( 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. - *

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 + * Customize the handling of {@link HttpMessageNotReadableException}. + *

By default this method creates a {@link ProblemDetail} with the status + * and a short detail message, and then delegates to + * {@link #handleExceptionInternal}. + * @param ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleHttpMessageNotReadable( 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. - *

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 + * Customize the handling of {@link HttpMessageNotWritableException}. + *

By default this method creates a {@link ProblemDetail} with the status + * and a short detail message, and then delegates to + * {@link #handleExceptionInternal}. + * @param ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleHttpMessageNotWritable( 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. - *

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 + * Customize the handling of {@link BindException}. + *

By default this method creates a {@link ProblemDetail} with the status + * and a short detail message, and then delegates to + * {@link #handleExceptionInternal}. + * @param ex the exception to handle + * @param headers the headers to use for the response + * @param status the status code to use for the response * @param request the current request - * @return {@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 protected ResponseEntity handleBindException( 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. - *

The default implementation sets the {@link WebUtils#ERROR_EXCEPTION_ATTRIBUTE} - * request attribute and creates a {@link ResponseEntity} from the given - * body, headers, and status. - * @param ex the exception - * @param body the body for the response - * @param headers the headers for the response - * @param statusCode the response status - * @param webRequest the current request - * @return {@code ResponseEntity} or {@code null} if response is committed + * Internal handler method that all others in this class delegate to, for + * common handling, and for the creation of a {@link ResponseEntity}. + *

The default implementation does the following: + *

    + *
  • return {@code null} if response is already committed + *
  • set the {@code "jakarta.servlet.error.exception"} request attribute + * if the response status is 500 (INTERNAL_SERVER_ERROR). + *
  • extract the {@link ErrorResponse#getBody() body} from + * {@link ErrorResponse} exceptions, if the {@code body} is {@code null}. + *
+ * @param ex the exception to handle + * @param body the body to use for the response + * @param headers the headers to use for the response + * @param statusCode the status code to use for the response + * @param request the current request + * @return a {@code ResponseEntity} for the response to use, possibly + * {@code null} when the response is already committed */ @Nullable protected ResponseEntity handleExceptionInternal( - Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest 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(); if (response != null && response.isCommitted()) { if (logger.isWarnEnabled()) { - logger.warn("Ignoring exception, response committed. : " + ex); + logger.warn("Response already committed. Ignoring: " + ex); } return null; } } - if (HttpStatus.INTERNAL_SERVER_ERROR.equals(statusCode)) { - webRequest.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); + if (statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR)) { + request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); } if (body == null && ex instanceof ErrorResponse errorResponse) { body = errorResponse.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 createResponseEntity( + @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) { + return new ResponseEntity<>(body, headers, statusCode); }