Browse Source

Add WebFlux equivalent of ResponseEntityExceptionHandler

Closes gh-28665
pull/28821/head
rstoyanchev 4 years ago
parent
commit
263811ecfa
  1. 335
      spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandler.java
  2. 231
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java
  3. 28
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java
  4. 105
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java
  5. 12
      src/docs/asciidoc/web/webflux.adoc

335
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandler.java

@ -0,0 +1,335 @@ @@ -0,0 +1,335 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.result.method.annotation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.web.ErrorResponse;
import org.springframework.web.ErrorResponseException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.MissingRequestValueException;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerErrorException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsatisfiedRequestParameterException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
/**
* A class with an {@code @ExceptionHandler} method that handles all Spring
* WebFlux raised exceptions by returning a {@link ResponseEntity} with
* RFC 7807 formatted error details in the body.
*
* <p>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 6.0
*/
public abstract class ResponseEntityExceptionHandler {
/**
* Common logger for use in subclasses.
*/
protected final Log logger = LogFactory.getLog(getClass());
/**
* Handle all exceptions raised within Spring MVC handling of the request .
* @param ex the exception to handle
* @param exchange the current request-response
*/
@ExceptionHandler({
MethodNotAllowedException.class,
NotAcceptableStatusException.class,
UnsupportedMediaTypeStatusException.class,
MissingRequestValueException.class,
UnsatisfiedRequestParameterException.class,
WebExchangeBindException.class,
ServerWebInputException.class,
ServerErrorException.class,
ResponseStatusException.class,
ErrorResponseException.class
})
public final Mono<ResponseEntity<Object>> handleException(Exception ex, ServerWebExchange exchange) {
HttpHeaders headers = new HttpHeaders();
if (ex instanceof MethodNotAllowedException theEx) {
return handleMethodNotAllowedException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
}
else if (ex instanceof NotAcceptableStatusException theEx) {
return handleNotAcceptableStatusException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
}
else if (ex instanceof UnsupportedMediaTypeStatusException theEx) {
return handleUnsupportedMediaTypeStatusException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
}
else if (ex instanceof MissingRequestValueException theEx) {
return handleMissingRequestValueException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
}
else if (ex instanceof UnsatisfiedRequestParameterException theEx) {
return handleUnsatisfiedRequestParameterException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
}
else if (ex instanceof WebExchangeBindException theEx) {
return handleWebExchangeBindException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
}
else if (ex instanceof ServerWebInputException theEx) {
return handleServerWebInputException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
}
else if (ex instanceof ServerErrorException theEx) {
return handleServerErrorException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
}
else if (ex instanceof ResponseStatusException theEx) {
return handleResponseStatusException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
}
else if (ex instanceof ErrorResponseException theEx) {
return handleErrorResponseException(theEx, theEx.getHeaders(), theEx.getStatusCode(), exchange);
}
else {
if (logger.isWarnEnabled()) {
logger.warn("Unexpected exception type: " + ex.getClass().getName());
}
return Mono.error(ex);
}
}
/**
* Customize the handling of {@link MethodNotAllowedException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleMethodNotAllowedException(
MethodNotAllowedException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Customize the handling of {@link NotAcceptableStatusException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleNotAcceptableStatusException(
NotAcceptableStatusException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Customize the handling of {@link UnsupportedMediaTypeStatusException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleUnsupportedMediaTypeStatusException(
UnsupportedMediaTypeStatusException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Customize the handling of {@link MissingRequestValueException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleMissingRequestValueException(
MissingRequestValueException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Customize the handling of {@link UnsatisfiedRequestParameterException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleUnsatisfiedRequestParameterException(
UnsatisfiedRequestParameterException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Customize the handling of {@link WebExchangeBindException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleWebExchangeBindException(
WebExchangeBindException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Customize the handling of {@link ServerWebInputException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleServerWebInputException(
ServerWebInputException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Customize the handling of any {@link ResponseStatusException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleResponseStatusException(
ResponseStatusException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Customize the handling of {@link ServerErrorException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleServerErrorException(
ServerErrorException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Customize the handling of any {@link ErrorResponseException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleErrorResponseException(
ErrorResponseException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Internal handler method that all others in this class delegate to, for
* common handling, and for the creation of a {@link ResponseEntity}.
* <p>The default implementation does the following:
* <ul>
* <li>return {@code null} if response is already committed
* <li>set the {@code "jakarta.servlet.error.exception"} request attribute
* if the response status is 500 (INTERNAL_SERVER_ERROR).
* <li>extract the {@link ErrorResponse#getBody() body} from
* {@link ErrorResponse} exceptions, if the {@code body} is {@code null}.
* </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 status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleExceptionInternal(
Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
if (body == null && ex instanceof ErrorResponse errorResponse) {
body = errorResponse.getBody();
}
return createResponseEntity(body, headers, status, exchange);
}
/**
* 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 status the status code to use for the response
* @param exchange the current request and response
* @return a {@code Mono} with the created {@code ResponseEntity}
* @since 6.0
*/
protected Mono<ResponseEntity<Object>> createResponseEntity(
@Nullable Object body, HttpHeaders headers, HttpStatusCode status, ServerWebExchange exchange) {
return Mono.just(new ResponseEntity<>(body, headers, status));
}
}

231
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java

@ -0,0 +1,231 @@ @@ -0,0 +1,231 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.result.method.annotation;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.ErrorResponseException;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.MissingRequestValueException;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerErrorException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsatisfiedRequestParameterException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
import org.springframework.web.testfixture.server.MockServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ResponseEntityExceptionHandler}.
*
* @author Rossen Stoyanchev
*/
public class ResponseEntityExceptionHandlerTests {
private final ResponseEntityExceptionHandler exceptionHandler = new GlobalExceptionHandler();
private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
@Test
void handleMethodNotAllowedException() {
ResponseEntity<ProblemDetail> entity = testException(
new MethodNotAllowedException(HttpMethod.PATCH, List.of(HttpMethod.GET, HttpMethod.POST)));
assertThat(entity.getHeaders().getFirst(HttpHeaders.ALLOW)).isEqualTo("GET,POST");
}
@Test
void handleNotAcceptableStatusException() {
ResponseEntity<ProblemDetail> entity = testException(
new NotAcceptableStatusException(List.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)));
assertThat(entity.getHeaders().getFirst(HttpHeaders.ACCEPT)).isEqualTo("application/json, application/xml");
}
@Test
void handleUnsupportedMediaTypeStatusException() {
ResponseEntity<ProblemDetail> entity = testException(
new UnsupportedMediaTypeStatusException(MediaType.APPLICATION_JSON, List.of(MediaType.APPLICATION_XML)));
assertThat(entity.getHeaders().getFirst(HttpHeaders.ACCEPT)).isEqualTo("application/xml");
}
@Test
void handleMissingRequestValueException() {
testException(new MissingRequestValueException("id", String.class, "cookie", null));
}
@Test
void handleUnsatisfiedRequestParameterException() {
testException(new UnsatisfiedRequestParameterException(Collections.emptyList(), new LinkedMultiValueMap<>()));
}
@Test
void handleWebExchangeBindException() {
testException(new WebExchangeBindException(null, null));
}
@Test
void handleServerWebInputException() {
testException(new ServerWebInputException(""));
}
@Test
void handleServerErrorException() {
testException(new ServerErrorException("", (Method) null, null));
}
@Test
void handleResponseStatusException() {
testException(new ResponseStatusException(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED));
}
@Test
void handleErrorResponseException() {
testException(new ErrorResponseException(HttpStatus.CONFLICT));
}
@SuppressWarnings("unchecked")
private ResponseEntity<ProblemDetail> testException(ErrorResponseException exception) {
ResponseEntity<?> responseEntity =
this.exceptionHandler.handleException(exception, this.exchange).block();
assertThat(responseEntity).isNotNull();
assertThat(responseEntity.getStatusCode()).isEqualTo(exception.getStatusCode());
assertThat(responseEntity.getBody()).isNotNull().isInstanceOf(ProblemDetail.class);
ProblemDetail body = (ProblemDetail) responseEntity.getBody();
assertThat(body.getType()).isEqualTo(URI.create(exception.getClass().getName()));
return (ResponseEntity<ProblemDetail>) responseEntity;
}
private static class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private Mono<ResponseEntity<Object>> handleAndSetTypeToExceptionName(
ErrorResponseException ex, HttpHeaders headers, HttpStatusCode status, ServerWebExchange exchange) {
ProblemDetail body = ex.getBody();
body.setType(URI.create(ex.getClass().getName()));
return handleExceptionInternal(ex, body, headers, status, exchange);
}
@Override
protected Mono<ResponseEntity<Object>> handleMethodNotAllowedException(
MethodNotAllowedException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
@Override
protected Mono<ResponseEntity<Object>> handleNotAcceptableStatusException(
NotAcceptableStatusException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
@Override
protected Mono<ResponseEntity<Object>> handleUnsupportedMediaTypeStatusException(
UnsupportedMediaTypeStatusException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
@Override
protected Mono<ResponseEntity<Object>> handleMissingRequestValueException(
MissingRequestValueException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
@Override
protected Mono<ResponseEntity<Object>> handleUnsatisfiedRequestParameterException(
UnsatisfiedRequestParameterException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
@Override
protected Mono<ResponseEntity<Object>> handleWebExchangeBindException(
WebExchangeBindException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
@Override
protected Mono<ResponseEntity<Object>> handleServerWebInputException(
ServerWebInputException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
@Override
protected Mono<ResponseEntity<Object>> handleResponseStatusException(
ResponseStatusException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
@Override
protected Mono<ResponseEntity<Object>> handleServerErrorException(
ServerErrorException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
@Override
protected Mono<ResponseEntity<Object>> handleErrorResponseException(
ErrorResponseException ex, HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
}
}

28
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java

@ -51,7 +51,7 @@ import org.springframework.web.util.WebUtils; @@ -51,7 +51,7 @@ import org.springframework.web.util.WebUtils;
/**
* A class with an {@code @ExceptionHandler} method that handles all Spring MVC
* raised exceptions by returning a {@link ResponseEntity} with RFC-7807
* raised exceptions by returning a {@link ResponseEntity} with RFC 7807
* formatted error details in the body.
*
* <p>Convenient as a base class of an {@link ControllerAdvice @ControllerAdvice}
@ -63,8 +63,6 @@ import org.springframework.web.util.WebUtils; @@ -63,8 +63,6 @@ import org.springframework.web.util.WebUtils;
*
* @author Rossen Stoyanchev
* @since 3.2
* @see #handleException(Exception, WebRequest)
* @see org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
*/
public abstract class ResponseEntityExceptionHandler {
@ -143,8 +141,8 @@ public abstract class ResponseEntityExceptionHandler { @@ -143,8 +141,8 @@ public abstract class ResponseEntityExceptionHandler {
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);
else if (ex instanceof ErrorResponseException subEx) {
return handleErrorResponseException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
}
// Lower level exceptions, and exceptions used symmetrically on client and server
@ -166,7 +164,7 @@ public abstract class ResponseEntityExceptionHandler { @@ -166,7 +164,7 @@ public abstract class ResponseEntityExceptionHandler {
}
else {
// Unknown exception, typically a wrapper with a common MVC exception as cause
// (since @ExceptionHandler type declarations also match first-level causes):
// (since @ExceptionHandler type declarations also match nested causes):
// We only deal with top-level MVC exceptions here, so let's rethrow the given
// exception for further processing through the HandlerExceptionResolver chain.
throw ex;
@ -347,6 +345,24 @@ public abstract class ResponseEntityExceptionHandler { @@ -347,6 +345,24 @@ public abstract class ResponseEntityExceptionHandler {
return handleExceptionInternal(ex, null, headers, status, request);
}
/**
* Customize the handling of any {@link ErrorResponseException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param request the current request
* @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
* @since 6.0
*/
@Nullable
protected ResponseEntity<Object> handleErrorResponseException(
ErrorResponseException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return handleExceptionInternal(ex, null, headers, status, request);
}
/**
* Customize the handling of {@link ConversionNotSupportedException}.
* <p>By default this method creates a {@link ProblemDetail} with the status

105
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java

@ -16,11 +16,9 @@ @@ -16,11 +16,9 @@
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;
import jakarta.servlet.ServletException;
import org.junit.jupiter.api.Test;
@ -36,7 +34,6 @@ import org.springframework.http.MediaType; @@ -36,7 +34,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
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;
@ -56,6 +53,7 @@ import org.springframework.web.context.request.async.AsyncRequestTimeoutExceptio @@ -56,6 +53,7 @@ import org.springframework.web.context.request.async.AsyncRequestTimeoutExceptio
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
@ -65,19 +63,19 @@ import org.springframework.web.testfixture.servlet.MockServletConfig; @@ -65,19 +63,19 @@ import org.springframework.web.testfixture.servlet.MockServletConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test fixture for {@link ResponseEntityExceptionHandler}.
* Unit tests for {@link ResponseEntityExceptionHandler}.
*
* @author Rossen Stoyanchev
*/
public class ResponseEntityExceptionHandlerTests {
private ResponseEntityExceptionHandler exceptionHandlerSupport = new ApplicationExceptionHandler();
private final ResponseEntityExceptionHandler exceptionHandler = new ApplicationExceptionHandler();
private DefaultHandlerExceptionResolver defaultExceptionResolver = new DefaultHandlerExceptionResolver();
private final DefaultHandlerExceptionResolver exceptionResolver = new DefaultHandlerExceptionResolver();
private MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/");
private MockHttpServletResponse servletResponse = new MockHttpServletResponse();
private final MockHttpServletResponse servletResponse = new MockHttpServletResponse();
private WebRequest request = new ServletWebRequest(this.servletRequest, this.servletResponse);
@ -101,21 +99,19 @@ public class ResponseEntityExceptionHandlerTests { @@ -101,21 +99,19 @@ public class ResponseEntityExceptionHandlerTests {
@Test
public void httpRequestMethodNotSupported() {
List<String> supported = Arrays.asList("POST", "DELETE");
Exception ex = new HttpRequestMethodNotSupportedException("GET", supported);
ResponseEntity<Object> entity =
testException(new HttpRequestMethodNotSupportedException("GET", List.of("POST", "DELETE")));
ResponseEntity<Object> responseEntity = testException(ex);
assertThat(responseEntity.getHeaders().getAllow()).isEqualTo(Set.of(HttpMethod.POST, HttpMethod.DELETE));
assertThat(entity.getHeaders().getFirst(HttpHeaders.ALLOW)).isEqualTo("POST, DELETE");
}
@Test
public void handleHttpMediaTypeNotSupported() {
List<MediaType> acceptable = Arrays.asList(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML);
Exception ex = new HttpMediaTypeNotSupportedException(MediaType.APPLICATION_JSON, acceptable);
ResponseEntity<Object> entity = testException(new HttpMediaTypeNotSupportedException(
MediaType.APPLICATION_JSON, List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML)));
ResponseEntity<Object> responseEntity = testException(ex);
assertThat(responseEntity.getHeaders().getAccept()).isEqualTo(acceptable);
assertThat(responseEntity.getHeaders().getAcceptPatch()).isEmpty();
assertThat(entity.getHeaders().getFirst(HttpHeaders.ACCEPT)).isEqualTo("application/atom+xml, application/xml");
assertThat(entity.getHeaders().getAcceptPatch()).isEmpty();
}
@Test
@ -123,92 +119,80 @@ public class ResponseEntityExceptionHandlerTests { @@ -123,92 +119,80 @@ public class ResponseEntityExceptionHandlerTests {
this.servletRequest = new MockHttpServletRequest("PATCH", "/");
this.request = new ServletWebRequest(this.servletRequest, this.servletResponse);
List<MediaType> acceptable = Arrays.asList(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML);
Exception ex = new HttpMediaTypeNotSupportedException(MediaType.APPLICATION_JSON, acceptable, HttpMethod.PATCH);
ResponseEntity<Object> entity = testException(
new HttpMediaTypeNotSupportedException(
MediaType.APPLICATION_JSON,
List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML),
HttpMethod.PATCH));
ResponseEntity<Object> responseEntity = testException(ex);
assertThat(responseEntity.getHeaders().getAccept()).isEqualTo(acceptable);
assertThat(responseEntity.getHeaders().getAcceptPatch()).isEqualTo(acceptable);
HttpHeaders headers = entity.getHeaders();
assertThat(headers.getFirst(HttpHeaders.ACCEPT)).isEqualTo("application/atom+xml, application/xml");
assertThat(headers.getFirst(HttpHeaders.ACCEPT)).isEqualTo("application/atom+xml, application/xml");
assertThat(headers.getFirst(HttpHeaders.ACCEPT_PATCH)).isEqualTo("application/atom+xml, application/xml");
}
@Test
public void httpMediaTypeNotAcceptable() {
Exception ex = new HttpMediaTypeNotAcceptableException("");
testException(ex);
testException(new HttpMediaTypeNotAcceptableException(""));
}
@Test
public void missingPathVariable() throws NoSuchMethodException {
Method method = getClass().getDeclaredMethod("handle", String.class);
MethodParameter parameter = new MethodParameter(method, 0);
Exception ex = new MissingPathVariableException("param", parameter);
testException(ex);
testException(new MissingPathVariableException("param",
new MethodParameter(getClass().getDeclaredMethod("handle", String.class), 0)));
}
@Test
public void missingServletRequestParameter() {
Exception ex = new MissingServletRequestParameterException("param", "type");
testException(ex);
testException(new MissingServletRequestParameterException("param", "type"));
}
@Test
public void servletRequestBindingException() {
Exception ex = new ServletRequestBindingException("message");
testException(ex);
testException(new ServletRequestBindingException("message"));
}
@Test
public void conversionNotSupported() {
Exception ex = new ConversionNotSupportedException(new Object(), Object.class, null);
testException(ex);
testException(new ConversionNotSupportedException(new Object(), Object.class, null));
}
@Test
public void typeMismatch() {
Exception ex = new TypeMismatchException("foo", String.class);
testException(ex);
testException(new TypeMismatchException("foo", String.class));
}
@Test
@SuppressWarnings("deprecation")
public void httpMessageNotReadable() {
Exception ex = new HttpMessageNotReadableException("message");
testException(ex);
testException(new HttpMessageNotReadableException("message"));
}
@Test
public void httpMessageNotWritable() {
Exception ex = new HttpMessageNotWritableException("");
testException(ex);
testException(new HttpMessageNotWritableException(""));
}
@Test
public void methodArgumentNotValid() throws Exception {
Exception ex = new MethodArgumentNotValidException(
testException(new MethodArgumentNotValidException(
new MethodParameter(getClass().getDeclaredMethod("handle", String.class), 0),
new MapBindingResult(Collections.emptyMap(), "name"));
testException(ex);
new MapBindingResult(Collections.emptyMap(), "name")));
}
@Test
public void missingServletRequestPart() {
Exception ex = new MissingServletRequestPartException("partName");
testException(ex);
testException(new MissingServletRequestPartException("partName"));
}
@Test
public void bindException() {
Exception ex = new BindException(new Object(), "name");
testException(ex);
testException(new BindException(new Object(), "name"));
}
@Test
public void noHandlerFoundException() {
ServletServerHttpRequest req = new ServletServerHttpRequest(
new MockHttpServletRequest("GET","/resource"));
Exception ex = new NoHandlerFoundException(req.getMethod().toString(),
req.getServletRequest().getRequestURI(),req.getHeaders());
testException(ex);
testException(new NoHandlerFoundException("GET", "/resource", HttpHeaders.EMPTY));
}
@Test
@ -244,8 +228,11 @@ public class ResponseEntityExceptionHandlerTests { @@ -244,8 +228,11 @@ public class ResponseEntityExceptionHandlerTests {
resolver.setApplicationContext(ctx);
resolver.afterPropertiesSet();
IllegalStateException ex = new IllegalStateException(new ServletRequestBindingException("message"));
assertThat(resolver.resolveException(this.servletRequest, this.servletResponse, null, ex)).isNull();
ModelAndView mav = resolver.resolveException(
this.servletRequest, this.servletResponse, null,
new IllegalStateException(new ServletRequestBindingException("message")));
assertThat(mav).isNull();
}
@Test
@ -287,18 +274,18 @@ public class ResponseEntityExceptionHandlerTests { @@ -287,18 +274,18 @@ public class ResponseEntityExceptionHandlerTests {
private ResponseEntity<Object> testException(Exception ex) {
try {
ResponseEntity<Object> responseEntity = this.exceptionHandlerSupport.handleException(ex, this.request);
ResponseEntity<Object> entity = this.exceptionHandler.handleException(ex, this.request);
// SPR-9653
if (HttpStatus.INTERNAL_SERVER_ERROR.equals(responseEntity.getStatusCode())) {
if (HttpStatus.INTERNAL_SERVER_ERROR.equals(entity.getStatusCode())) {
assertThat(this.servletRequest.getAttribute("jakarta.servlet.error.exception")).isSameAs(ex);
}
this.defaultExceptionResolver.resolveException(this.servletRequest, this.servletResponse, null, ex);
assertThat(responseEntity.getStatusCode().value()).isEqualTo(this.servletResponse.getStatus());
// Verify DefaultHandlerExceptionResolver would set the same status
this.exceptionResolver.resolveException(this.servletRequest, this.servletResponse, null, ex);
assertThat(entity.getStatusCode().value()).isEqualTo(this.servletResponse.getStatus());
return responseEntity;
return entity;
}
catch (Exception ex2) {
throw new IllegalStateException("handleException threw exception", ex2);

12
src/docs/asciidoc/web/webflux.adoc

@ -3421,10 +3421,14 @@ of error details in the response body is application-specific. However, a @@ -3421,10 +3421,14 @@ of error details in the response body is application-specific. However, a
value to set the status and the body of the response. Such methods can also be declared
in `@ControllerAdvice` classes to apply them globally.
NOTE: Note that Spring WebFlux does not have an equivalent for the Spring MVC
`ResponseEntityExceptionHandler`, because WebFlux raises only `ResponseStatusException`
(or subclasses thereof), and those do not need to be translated to
an HTTP status code.
Applications that implement global exception handling with error details in the response
body should consider extending
{api-spring-framework}/web/reactive/result/method/annotation/ResponseEntityExceptionHandler.html[`ResponseEntityExceptionHandler`],
which provides handling for exceptions that Spring MVC raises and provides hooks to
customize the response body. To make use of this, create a subclass of
`ResponseEntityExceptionHandler`, annotate it with `@ControllerAdvice`, override the
necessary methods, and declare it as a Spring bean.

Loading…
Cancel
Save