MessageSource support for Spring MVC and WebFlux exceptions
See gh-28814
This commit is contained in:
@@ -19,9 +19,12 @@ package org.springframework.web;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.context.support.StaticMessageSource;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -32,7 +35,6 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingMatrixVariableException;
|
||||
import org.springframework.web.bind.MissingPathVariableException;
|
||||
@@ -46,13 +48,13 @@ import org.springframework.web.multipart.support.MissingServletRequestPartExcept
|
||||
import org.springframework.web.server.MethodNotAllowedException;
|
||||
import org.springframework.web.server.MissingRequestValueException;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ServerErrorException;
|
||||
import org.springframework.web.server.UnsatisfiedRequestParameterException;
|
||||
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
|
||||
import org.springframework.web.testfixture.method.ResolvableMethod;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests that verify the HTTP response details exposed by exceptions in the
|
||||
* {@link ErrorResponse} hierarchy.
|
||||
@@ -71,12 +73,12 @@ public class ErrorResponseExceptionTests {
|
||||
List<MediaType> mediaTypes =
|
||||
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
|
||||
|
||||
ErrorResponse ex = new HttpMediaTypeNotSupportedException(
|
||||
HttpMediaTypeNotSupportedException ex = new HttpMediaTypeNotSupportedException(
|
||||
MediaType.APPLICATION_XML, mediaTypes, HttpMethod.PATCH, "Custom message");
|
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
|
||||
assertDetail(ex, "Content-Type 'application/xml' is not supported.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getContentType(), ex.getSupportedMediaTypes()});
|
||||
|
||||
HttpHeaders headers = ex.getHeaders();
|
||||
assertThat(headers.getAccept()).isEqualTo(mediaTypes);
|
||||
@@ -89,9 +91,10 @@ public class ErrorResponseExceptionTests {
|
||||
ErrorResponse ex = new HttpMediaTypeNotSupportedException(
|
||||
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'");
|
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
|
||||
assertDetail(ex, "Could not parse Content-Type.");
|
||||
assertDetailMessageCode(ex, "parseError", null);
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@@ -99,11 +102,11 @@ public class ErrorResponseExceptionTests {
|
||||
void httpMediaTypeNotAcceptableException() {
|
||||
|
||||
List<MediaType> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
|
||||
ErrorResponse ex = new HttpMediaTypeNotAcceptableException(mediaTypes);
|
||||
|
||||
HttpMediaTypeNotAcceptableException ex = new HttpMediaTypeNotAcceptableException(mediaTypes);
|
||||
|
||||
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE);
|
||||
assertDetail(ex, "Acceptable representations: 'application/json, application/cbor'.");
|
||||
assertDetail(ex, "Acceptable representations: [application/json, application/cbor].");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getSupportedMediaTypes()});
|
||||
|
||||
assertThat(ex.getHeaders()).hasSize(1);
|
||||
assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes);
|
||||
@@ -115,9 +118,10 @@ public class ErrorResponseExceptionTests {
|
||||
ErrorResponse ex = new HttpMediaTypeNotAcceptableException(
|
||||
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'");
|
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE);
|
||||
assertDetail(ex, "Could not parse Accept header.");
|
||||
assertDetailMessageCode(ex, "parseError", null);
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@@ -125,22 +129,23 @@ public class ErrorResponseExceptionTests {
|
||||
void asyncRequestTimeoutException() {
|
||||
|
||||
ErrorResponse ex = new AsyncRequestTimeoutException();
|
||||
|
||||
assertDetailMessageCode(ex, null, null);
|
||||
|
||||
assertStatus(ex, HttpStatus.SERVICE_UNAVAILABLE);
|
||||
assertDetail(ex, null);
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpRequestMethodNotSupportedException() {
|
||||
|
||||
String[] supportedMethods = new String[] { "GET", "POST" };
|
||||
ErrorResponse ex = new HttpRequestMethodNotSupportedException("PUT", supportedMethods, "Custom message");
|
||||
|
||||
HttpRequestMethodNotSupportedException ex =
|
||||
new HttpRequestMethodNotSupportedException("PUT", Arrays.asList("GET", "POST"));
|
||||
|
||||
assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED);
|
||||
assertDetail(ex, "Method 'PUT' is not supported.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getMethod(), ex.getSupportedHttpMethods()});
|
||||
|
||||
assertThat(ex.getHeaders()).hasSize(1);
|
||||
assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST);
|
||||
@@ -149,90 +154,101 @@ public class ErrorResponseExceptionTests {
|
||||
@Test
|
||||
void missingRequestHeaderException() {
|
||||
|
||||
ErrorResponse ex = new MissingRequestHeaderException("Authorization", this.methodParameter);
|
||||
|
||||
MissingRequestHeaderException ex = new MissingRequestHeaderException("Authorization", this.methodParameter);
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Required header 'Authorization' is not present.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getHeaderName()});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingServletRequestParameterException() {
|
||||
|
||||
ErrorResponse ex = new MissingServletRequestParameterException("query", "String");
|
||||
|
||||
MissingServletRequestParameterException ex = new MissingServletRequestParameterException("query", "String");
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Required parameter 'query' is not present.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getParameterName()});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingMatrixVariableException() {
|
||||
|
||||
ErrorResponse ex = new MissingMatrixVariableException("region", this.methodParameter);
|
||||
MissingMatrixVariableException ex = new MissingMatrixVariableException("region", this.methodParameter);
|
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Required path parameter 'region' is not present.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getVariableName()});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingPathVariableException() {
|
||||
|
||||
ErrorResponse ex = new MissingPathVariableException("id", this.methodParameter);
|
||||
|
||||
MissingPathVariableException ex = new MissingPathVariableException("id", this.methodParameter);
|
||||
|
||||
assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
assertDetail(ex, "Required path variable 'id' is not present.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getVariableName()});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingRequestCookieException() {
|
||||
|
||||
ErrorResponse ex = new MissingRequestCookieException("oreo", this.methodParameter);
|
||||
|
||||
MissingRequestCookieException ex = new MissingRequestCookieException("oreo", this.methodParameter);
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Required cookie 'oreo' is not present.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getCookieName()});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsatisfiedServletRequestParameterException() {
|
||||
|
||||
ErrorResponse ex = new UnsatisfiedServletRequestParameterException(
|
||||
UnsatisfiedServletRequestParameterException ex = new UnsatisfiedServletRequestParameterException(
|
||||
new String[] { "foo=bar", "bar=baz" }, Collections.singletonMap("q", new String[] {"1"}));
|
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Invalid request parameters.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {List.of("\"foo=bar, bar=baz\"")});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingServletRequestPartException() {
|
||||
|
||||
ErrorResponse ex = new MissingServletRequestPartException("file");
|
||||
MissingServletRequestPartException ex = new MissingServletRequestPartException("file");
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Required part 'file' is not present.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getRequestPartName()});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void methodArgumentNotValidException() {
|
||||
|
||||
BindingResult bindingResult = new BindException(new Object(), "object");
|
||||
bindingResult.addError(new FieldError("object", "field", "message"));
|
||||
MessageSourceTestHelper messageSourceHelper = new MessageSourceTestHelper(MethodArgumentNotValidException.class);
|
||||
BindingResult bindingResult = messageSourceHelper.initBindingResult();
|
||||
|
||||
ErrorResponse ex = new MethodArgumentNotValidException(this.methodParameter, bindingResult);
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Invalid request content.");
|
||||
messageSourceHelper.assertDetailMessage(ex);
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@@ -242,11 +258,12 @@ public class ErrorResponseExceptionTests {
|
||||
List<MediaType> mediaTypes =
|
||||
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
|
||||
|
||||
ErrorResponse ex = new UnsupportedMediaTypeStatusException(
|
||||
UnsupportedMediaTypeStatusException ex = new UnsupportedMediaTypeStatusException(
|
||||
MediaType.APPLICATION_XML, mediaTypes, HttpMethod.PATCH);
|
||||
|
||||
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
|
||||
assertDetail(ex, "Content-Type 'application/xml' is not supported.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getContentType(), ex.getSupportedMediaTypes()});
|
||||
|
||||
HttpHeaders headers = ex.getHeaders();
|
||||
assertThat(headers.getAccept()).isEqualTo(mediaTypes);
|
||||
@@ -261,19 +278,20 @@ public class ErrorResponseExceptionTests {
|
||||
|
||||
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
|
||||
assertDetail(ex, "Could not parse Content-Type.");
|
||||
assertDetailMessageCode(ex, "parseError", null);
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void notAcceptableStatusException() {
|
||||
|
||||
List<MediaType> mediaTypes =
|
||||
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
|
||||
|
||||
ErrorResponse ex = new NotAcceptableStatusException(mediaTypes);
|
||||
List<MediaType> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR);
|
||||
NotAcceptableStatusException ex = new NotAcceptableStatusException(mediaTypes);
|
||||
|
||||
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE);
|
||||
assertDetail(ex, "Acceptable representations: 'application/json, application/cbor'.");
|
||||
assertDetail(ex, "Acceptable representations: [application/json, application/cbor].");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getSupportedMediaTypes()});
|
||||
|
||||
assertThat(ex.getHeaders()).hasSize(1);
|
||||
assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes);
|
||||
@@ -285,45 +303,65 @@ public class ErrorResponseExceptionTests {
|
||||
ErrorResponse ex = new NotAcceptableStatusException(
|
||||
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'");
|
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE);
|
||||
assertDetail(ex, "Could not parse Accept header.");
|
||||
assertDetailMessageCode(ex, "parseError", null);
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void serverErrorException() {
|
||||
|
||||
ServerErrorException ex = new ServerErrorException("Failure", null);
|
||||
|
||||
assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
assertDetail(ex, null);
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getReason()});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingRequestValueException() {
|
||||
|
||||
ErrorResponse ex = new MissingRequestValueException(
|
||||
"foo", String.class, "header", this.methodParameter);
|
||||
MissingRequestValueException ex =
|
||||
new MissingRequestValueException("foo", String.class, "header", this.methodParameter);
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Required header 'foo' is not present.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getLabel(), ex.getName()});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsatisfiedRequestParameterException() {
|
||||
|
||||
ErrorResponse ex = new UnsatisfiedRequestParameterException(
|
||||
Arrays.asList("foo=bar", "bar=baz"),
|
||||
new LinkedMultiValueMap<>(Collections.singletonMap("q", Arrays.asList("1", "2"))));
|
||||
UnsatisfiedRequestParameterException ex =
|
||||
new UnsatisfiedRequestParameterException(
|
||||
Arrays.asList("foo=bar", "bar=baz"),
|
||||
new LinkedMultiValueMap<>(Collections.singletonMap("q", Arrays.asList("1", "2"))));
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Invalid request parameters.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getConditions()});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void webExchangeBindException() {
|
||||
|
||||
BindingResult bindingResult = new BindException(new Object(), "object");
|
||||
bindingResult.addError(new FieldError("object", "field", "message"));
|
||||
MessageSourceTestHelper messageSourceHelper = new MessageSourceTestHelper(WebExchangeBindException.class);
|
||||
BindingResult bindingResult = messageSourceHelper.initBindingResult();
|
||||
|
||||
ErrorResponse ex = new WebExchangeBindException(this.methodParameter, bindingResult);
|
||||
WebExchangeBindException ex = new WebExchangeBindException(this.methodParameter, bindingResult);
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST);
|
||||
assertDetail(ex, "Invalid request content.");
|
||||
messageSourceHelper.assertDetailMessage(ex);
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@@ -331,11 +369,11 @@ public class ErrorResponseExceptionTests {
|
||||
void methodNotAllowedException() {
|
||||
|
||||
List<HttpMethod> supportedMethods = Arrays.asList(HttpMethod.GET, HttpMethod.POST);
|
||||
ErrorResponse ex = new MethodNotAllowedException(HttpMethod.PUT, supportedMethods);
|
||||
|
||||
MethodNotAllowedException ex = new MethodNotAllowedException(HttpMethod.PUT, supportedMethods);
|
||||
|
||||
assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED);
|
||||
assertDetail(ex, "Supported methods: 'GET', 'POST'");
|
||||
assertDetail(ex, "Supported methods: [GET, POST]");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getHttpMethod(), supportedMethods});
|
||||
|
||||
assertThat(ex.getHeaders()).hasSize(1);
|
||||
assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST);
|
||||
@@ -344,11 +382,12 @@ public class ErrorResponseExceptionTests {
|
||||
@Test
|
||||
void methodNotAllowedExceptionWithoutSupportedMethods() {
|
||||
|
||||
ErrorResponse ex = new MethodNotAllowedException(HttpMethod.PUT, Collections.emptyList());
|
||||
|
||||
MethodNotAllowedException ex = new MethodNotAllowedException(HttpMethod.PUT, Collections.emptyList());
|
||||
|
||||
assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED);
|
||||
assertDetail(ex, "Request method 'PUT' is not supported.");
|
||||
assertDetailMessageCode(ex, null, new Object[] {ex.getHttpMethod(), Collections.emptyList()});
|
||||
|
||||
assertThat(ex.getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@@ -368,8 +407,64 @@ public class ErrorResponseExceptionTests {
|
||||
}
|
||||
}
|
||||
|
||||
private void assertDetailMessageCode(
|
||||
ErrorResponse ex, @Nullable String suffix, @Nullable Object[] arguments) {
|
||||
|
||||
assertThat(ex.getDetailMessageCode())
|
||||
.isEqualTo(ErrorResponse.getDefaultDetailMessageCode(((Exception) ex).getClass(), suffix));
|
||||
|
||||
if (arguments != null) {
|
||||
assertThat(ex.getDetailMessageArguments()).containsExactlyElementsOf(Arrays.asList(arguments));
|
||||
}
|
||||
else {
|
||||
assertThat(ex.getDetailMessageArguments()).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private void handle(String arg) {}
|
||||
|
||||
|
||||
private static class MessageSourceTestHelper {
|
||||
|
||||
private final String code;
|
||||
|
||||
public MessageSourceTestHelper(Class<? extends ErrorResponse> exceptionType) {
|
||||
this.code = "problemDetail." + exceptionType.getName();
|
||||
}
|
||||
|
||||
public BindingResult initBindingResult() {
|
||||
BindingResult bindingResult = new BindException(new TestBean(), "myBean");
|
||||
bindingResult.reject("bean.invalid.A", "Invalid bean message");
|
||||
bindingResult.reject("bean.invalid.B");
|
||||
bindingResult.rejectValue("name", "name.required", "Name is required");
|
||||
bindingResult.rejectValue("age", "age.min");
|
||||
return bindingResult;
|
||||
}
|
||||
|
||||
private void assertDetailMessage(ErrorResponse ex) {
|
||||
StaticMessageSource messageSource = new StaticMessageSource();
|
||||
messageSource.addMessage(this.code, Locale.UK, "Failures {0}. nested failures: {1}");
|
||||
messageSource.addMessage("bean.invalid.A", Locale.UK, "Bean A message");
|
||||
messageSource.addMessage("bean.invalid.B", Locale.UK, "Bean B message");
|
||||
messageSource.addMessage("name.required", Locale.UK, "Required name message");
|
||||
messageSource.addMessage("age.min", Locale.UK, "Minimum age message");
|
||||
|
||||
String message = messageSource.getMessage(
|
||||
ex.getDetailMessageCode(), ex.getDetailMessageArguments(), Locale.UK);
|
||||
|
||||
assertThat(message).isEqualTo("" +
|
||||
"Failures ['Invalid bean message', 'bean.invalid.B']. " +
|
||||
"nested failures: [name: 'Name is required', age: 'age.min']");
|
||||
|
||||
message = messageSource.getMessage(
|
||||
ex.getDetailMessageCode(), ex.getDetailMessageArguments(messageSource, Locale.UK), Locale.UK);
|
||||
|
||||
assertThat(message).isEqualTo("" +
|
||||
"Failures ['Bean A message', 'Bean B message']. " +
|
||||
"nested failures: [name: 'Required name message', age: 'Minimum age message']");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user