Support Content Negotiation with @ExceptionHandler
Prior to this commit, `@ExceptionHandler` annotated controller methods
could be mapped using the exception type declaration as an annotation
attribute, or as a method parameter.
While such methods support a wide variety of method arguments and return
types, it was not possible to declare the same exception type on
different methods (in the same controller/controller advice).
This commit adds a new `produces` attribute on `@ExceptionHandler`; with
that, applications can vary the HTTP response depending on the exception
type and the requested content-type by the client:
```
@ExceptionHandler(produces = "application/json")
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42));
}
@ExceptionHandler(produces = "text/html")
public String handle(IllegalArgumentException exc, Model model) {
model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42));
return "errorView";
}
```
This commit implements support in both Spring MVC and Spring WebFlux.
Closes gh-31936
This commit is contained in:
@@ -25,6 +25,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
@@ -33,28 +34,29 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Test fixture for {@link ExceptionHandlerMethodResolver} tests.
|
||||
* Tests for {@link ExceptionHandlerMethodResolver}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class ExceptionHandlerMethodResolverTests {
|
||||
|
||||
@Test
|
||||
void resolveMethodFromAnnotation() {
|
||||
void shouldResolveMethodFromAnnotationAttribute() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
IOException exception = new IOException();
|
||||
assertThat(resolver.resolveMethod(exception).getName()).isEqualTo("handleIOException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodFromArgument() {
|
||||
void shouldResolveMethodFromMethodArgument() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
IllegalArgumentException exception = new IllegalArgumentException();
|
||||
assertThat(resolver.resolveMethod(exception).getName()).isEqualTo("handleIllegalArgumentException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodExceptionSubType() {
|
||||
void shouldResolveMethodWithExceptionSubType() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
IOException ioException = new FileNotFoundException();
|
||||
assertThat(resolver.resolveMethod(ioException).getName()).isEqualTo("handleIOException");
|
||||
@@ -63,14 +65,14 @@ class ExceptionHandlerMethodResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodBestMatch() {
|
||||
void shouldResolveMethodWithExceptionBestMatch() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
SocketException exception = new SocketException();
|
||||
assertThat(resolver.resolveMethod(exception).getName()).isEqualTo("handleSocketException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodNoMatch() {
|
||||
void shouldNotResolveMethodWhenExceptionNoMatch() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
Exception exception = new Exception();
|
||||
assertThat(resolver.resolveMethod(exception)).as("1st lookup").isNull();
|
||||
@@ -78,7 +80,7 @@ class ExceptionHandlerMethodResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodExceptionCause() {
|
||||
void ShouldResolveMethodWithExceptionCause() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
|
||||
SocketException bindException = new BindException();
|
||||
@@ -90,24 +92,65 @@ class ExceptionHandlerMethodResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodInherited() {
|
||||
void shouldResolveMethodFromSuperClass() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(InheritedController.class);
|
||||
IOException exception = new IOException();
|
||||
assertThat(resolver.resolveMethod(exception).getName()).isEqualTo("handleIOException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ambiguousExceptionMapping() {
|
||||
void shouldThrowExceptionWhenAmbiguousExceptionMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(AmbiguousController.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noExceptionMapping() {
|
||||
void shouldThrowExceptionWhenNoExceptionMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(NoExceptionController.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveMethodWithMediaType() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(MediaTypeController.class);
|
||||
assertThat(resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.APPLICATION_JSON).getHandlerMethod().getName()).isEqualTo("handleJson");
|
||||
assertThat(resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.TEXT_HTML).getHandlerMethod().getName()).isEqualTo("handleHtml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveMethodWithCompatibleMediaType() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(MediaTypeController.class);
|
||||
assertThat(resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.parseMediaType("application/*")).getHandlerMethod().getName()).isEqualTo("handleJson");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFavorMethodWithExplicitAcceptAll() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(MediaTypeController.class);
|
||||
assertThat(resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.ALL).getHandlerMethod().getName()).isEqualTo("handleHtml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenInvalidMediaTypeMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(InvalidMediaTypeController.class))
|
||||
.withMessageContaining("Invalid media type [invalid-mediatype] declared on @ExceptionHandler");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenAmbiguousMediaTypeMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(AmbiguousMediaTypeController.class))
|
||||
.withMessageContaining("Ambiguous @ExceptionHandler method mapped for [ExceptionHandler{exceptionType=java.lang.IllegalArgumentException, mediaType=application/json}]")
|
||||
.withMessageContaining("AmbiguousMediaTypeController.handleJson()")
|
||||
.withMessageContaining("AmbiguousMediaTypeController.handleJsonToo()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveMethodWithMediaTypeFallback() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(MixedController.class);
|
||||
assertThat(resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.TEXT_HTML).getHandlerMethod().getName()).isEqualTo("handleOther");
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
static class ExceptionController {
|
||||
@@ -162,4 +205,58 @@ class ExceptionHandlerMethodResolverTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MediaTypeController {
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = "application/json")
|
||||
public void handleJson() {
|
||||
|
||||
}
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = {"text/html", "*/*"})
|
||||
public void handleHtml() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class AmbiguousMediaTypeController {
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = "application/json")
|
||||
public void handleJson() {
|
||||
|
||||
}
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = "application/json")
|
||||
public void handleJsonToo() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MixedController {
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = "application/json")
|
||||
public void handleJson() {
|
||||
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public void handleOther() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class InvalidMediaTypeController {
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = "invalid-mediatype")
|
||||
public void handle() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user