From b045e5baef0445e4feb16a5fc548bc4a7bf5555b Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 28 Feb 2022 11:20:07 +0000 Subject: [PATCH] Tests for ErrorResponse hierarchy to verify the output See gh-27052 --- .../web/ErrorResponseException.java | 2 +- .../HttpMediaTypeNotAcceptableException.java | 11 +- .../HttpMediaTypeNotSupportedException.java | 18 +- ...ttpRequestMethodNotSupportedException.java | 5 +- .../bind/MethodArgumentNotValidException.java | 10 +- .../bind/MissingMatrixVariableException.java | 2 +- .../bind/MissingPathVariableException.java | 2 +- .../bind/MissingRequestCookieException.java | 2 +- .../bind/MissingRequestHeaderException.java | 2 +- ...ssingServletRequestParameterException.java | 2 +- ...sfiedServletRequestParameterException.java | 6 +- .../support/WebExchangeBindException.java | 1 + .../MissingServletRequestPartException.java | 2 +- .../web/server/MethodNotAllowedException.java | 7 +- .../server/NotAcceptableStatusException.java | 8 +- .../web/server/ResponseStatusException.java | 1 - .../UnsupportedMediaTypeStatusException.java | 4 +- .../web/ErrorResponseExceptionTests.java | 349 ++++++++++++++++++ .../web/servlet/NoHandlerFoundException.java | 7 +- .../ResponseEntityExceptionHandler.java | 1 + 20 files changed, 400 insertions(+), 42 deletions(-) create mode 100644 spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java diff --git a/spring-web/src/main/java/org/springframework/web/ErrorResponseException.java b/spring-web/src/main/java/org/springframework/web/ErrorResponseException.java index ec84ecea2ee..04dbfd8e453 100644 --- a/spring-web/src/main/java/org/springframework/web/ErrorResponseException.java +++ b/spring-web/src/main/java/org/springframework/web/ErrorResponseException.java @@ -61,7 +61,7 @@ public class ErrorResponseException extends NestedRuntimeException implements Er * Constructor with a well-known {@link HttpStatus} and an optional cause. */ public ErrorResponseException(HttpStatus status, @Nullable Throwable cause) { - this(status.value(), null); + this(status.value(), cause); } /** diff --git a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java index 13da2c1130e..d405efa3920 100644 --- a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java +++ b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java @@ -17,6 +17,7 @@ package org.springframework.web; import java.util.List; +import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -39,15 +40,17 @@ public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException */ public HttpMediaTypeNotAcceptableException(String message) { super(message); - getBody().setDetail("Could not parse Accept header"); + getBody().setDetail("Could not parse Accept header."); } /** * Create a new HttpMediaTypeNotSupportedException. - * @param supportedMediaTypes the list of supported media types + * @param mediaTypes the list of supported media types */ - public HttpMediaTypeNotAcceptableException(List supportedMediaTypes) { - super("No acceptable representation", supportedMediaTypes); + public HttpMediaTypeNotAcceptableException(List mediaTypes) { + super("No acceptable representation", mediaTypes); + getBody().setDetail("Acceptable representations: " + + mediaTypes.stream().map(MediaType::toString).collect(Collectors.joining(", ", "'", "'")) + "."); } diff --git a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java index c1665fd6d5b..7a55ce2a8e2 100644 --- a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java +++ b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java @@ -51,29 +51,29 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException { super(message); this.contentType = null; this.httpMethod = null; - getBody().setDetail("Could not parse Content-Type"); + getBody().setDetail("Could not parse Content-Type."); } /** * Create a new HttpMediaTypeNotSupportedException. * @param contentType the unsupported content type - * @param supportedMediaTypes the list of supported media types + * @param mediaTypes the list of supported media types */ - public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType, List supportedMediaTypes) { - this(contentType, supportedMediaTypes, null); + public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType, List mediaTypes) { + this(contentType, mediaTypes, null); } /** * Create a new HttpMediaTypeNotSupportedException. * @param contentType the unsupported content type - * @param supportedMediaTypes the list of supported media types + * @param mediaTypes the list of supported media types * @param httpMethod the HTTP method of the request * @since 6.0 */ - public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType, - List supportedMediaTypes, @Nullable HttpMethod httpMethod) { + public HttpMediaTypeNotSupportedException( + @Nullable MediaType contentType, List mediaTypes, @Nullable HttpMethod httpMethod) { - this(contentType, supportedMediaTypes, httpMethod, + this(contentType, mediaTypes, httpMethod, "Content-Type " + (contentType != null ? "'" + contentType + "' " : "") + "is not supported"); } @@ -91,7 +91,7 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException { super(message, supportedMediaTypes); this.contentType = contentType; this.httpMethod = httpMethod; - getBody().setDetail("Content-Type " + this.contentType + " is not supported"); + getBody().setDetail("Content-Type '" + this.contentType + "' is not supported."); } diff --git a/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java b/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java index e17b0564c04..1f86b399b83 100644 --- a/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java +++ b/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java @@ -93,8 +93,9 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp super(msg); this.method = method; this.supportedMethods = supportedMethods; - this.body = ProblemDetail.forRawStatusCode(getRawStatusCode()) - .withDetail("Method '" + method + "' is not supported"); + + String detail = "Method '" + method + "' is not supported."; + this.body = ProblemDetail.forRawStatusCode(getRawStatusCode()).withDetail(detail); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java b/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java index bc404d5c6fa..876e1c235ee 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/MethodArgumentNotValidException.java @@ -48,7 +48,7 @@ public class MethodArgumentNotValidException extends BindException implements Er public MethodArgumentNotValidException(MethodParameter parameter, BindingResult bindingResult) { super(bindingResult); this.parameter = parameter; - this.body = ProblemDetail.forRawStatusCode(getRawStatusCode()).withDetail(initMessage(parameter)); + this.body = ProblemDetail.forRawStatusCode(getRawStatusCode()).withDetail("Invalid request content."); } @@ -71,13 +71,9 @@ public class MethodArgumentNotValidException extends BindException implements Er @Override public String getMessage() { - return initMessage(this.parameter); - } - - private String initMessage(MethodParameter parameter) { StringBuilder sb = new StringBuilder("Validation failed for argument [") - .append(parameter.getParameterIndex()).append("] in ") - .append(parameter.getExecutable().toGenericString()); + .append(this.parameter.getParameterIndex()).append("] in ") + .append(this.parameter.getExecutable().toGenericString()); BindingResult bindingResult = getBindingResult(); if (bindingResult.getErrorCount() > 1) { sb.append(" with ").append(bindingResult.getErrorCount()).append(" errors"); diff --git a/spring-web/src/main/java/org/springframework/web/bind/MissingMatrixVariableException.java b/spring-web/src/main/java/org/springframework/web/bind/MissingMatrixVariableException.java index 6be5f22762f..d1c1f0ea4f3 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/MissingMatrixVariableException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/MissingMatrixVariableException.java @@ -57,7 +57,7 @@ public class MissingMatrixVariableException extends MissingRequestValueException super("", missingAfterConversion); this.variableName = variableName; this.parameter = parameter; - getBody().setDetail("Required path parameter '" + this.variableName + "' is not present"); + getBody().setDetail("Required path parameter '" + this.variableName + "' is not present."); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/MissingPathVariableException.java b/spring-web/src/main/java/org/springframework/web/bind/MissingPathVariableException.java index 6c023b627e9..f7f3eba5d04 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/MissingPathVariableException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/MissingPathVariableException.java @@ -60,7 +60,7 @@ public class MissingPathVariableException extends MissingRequestValueException { super("", missingAfterConversion); this.variableName = variableName; this.parameter = parameter; - getBody().setDetail("Required URI variable '" + this.variableName + "' is not present"); + getBody().setDetail("Required path variable '" + this.variableName + "' is not present."); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/MissingRequestCookieException.java b/spring-web/src/main/java/org/springframework/web/bind/MissingRequestCookieException.java index 24f27afee70..284e211dcbd 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/MissingRequestCookieException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/MissingRequestCookieException.java @@ -57,7 +57,7 @@ public class MissingRequestCookieException extends MissingRequestValueException super("", missingAfterConversion); this.cookieName = cookieName; this.parameter = parameter; - getBody().setDetail("Required cookie '" + this.cookieName + "' is not present"); + getBody().setDetail("Required cookie '" + this.cookieName + "' is not present."); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/MissingRequestHeaderException.java b/spring-web/src/main/java/org/springframework/web/bind/MissingRequestHeaderException.java index 00c3b463826..2d30629a1ec 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/MissingRequestHeaderException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/MissingRequestHeaderException.java @@ -57,7 +57,7 @@ public class MissingRequestHeaderException extends MissingRequestValueException super("", missingAfterConversion); this.headerName = headerName; this.parameter = parameter; - getBody().setDetail("Required header '" + this.headerName + "' is not present"); + getBody().setDetail("Required header '" + this.headerName + "' is not present."); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java b/spring-web/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java index 2dfcf754b49..5088b7c4d64 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/MissingServletRequestParameterException.java @@ -52,7 +52,7 @@ public class MissingServletRequestParameterException extends MissingRequestValue super("", missingAfterConversion); this.parameterName = parameterName; this.parameterType = parameterType; - getBody().setDetail("Required parameter '" + this.parameterName + "' is not present"); + getBody().setDetail("Required parameter '" + this.parameterName + "' is not present."); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java b/spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java index feecdeef80f..224f0af0fdd 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java @@ -16,7 +16,6 @@ package org.springframework.web.bind; -import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -48,9 +47,7 @@ public class UnsatisfiedServletRequestParameterException extends ServletRequestB * @param actualParams the actual parameter Map associated with the ServletRequest */ public UnsatisfiedServletRequestParameterException(String[] paramConditions, Map actualParams) { - super(""); - this.paramConditions = Arrays.asList(paramConditions); - this.actualParams = actualParams; + this(List.of(paramConditions), actualParams); } /** @@ -66,6 +63,7 @@ public class UnsatisfiedServletRequestParameterException extends ServletRequestB Assert.notEmpty(paramConditions, "Parameter conditions must not be empty"); this.paramConditions = paramConditions; this.actualParams = actualParams; + getBody().setDetail("Invalid request parameters."); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java index 8c605fc6625..5df77ee8e68 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java @@ -49,6 +49,7 @@ public class WebExchangeBindException extends ServerWebInputException implements public WebExchangeBindException(MethodParameter parameter, BindingResult bindingResult) { super("Validation failure", parameter); this.bindingResult = bindingResult; + getBody().setDetail("Invalid request content."); } diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java b/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java index 6719f8e67bd..04595f8ad5c 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java @@ -40,7 +40,7 @@ public class MissingServletRequestPartException extends ServletRequestBindingExc * @param requestPartName the name of the missing part of the multipart request */ public MissingServletRequestPartException(String requestPartName) { - super("Required request part '" + requestPartName + "' is not present"); + super("Required part '" + requestPartName + "' is not present."); this.requestPartName = requestPartName; getBody().setDetail(getMessage()); } diff --git a/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java b/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java index be3271aea02..f152b1dd70e 100644 --- a/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java +++ b/spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -47,13 +48,17 @@ public class MethodNotAllowedException extends ResponseStatusException { } public MethodNotAllowedException(String method, @Nullable Collection supportedMethods) { - super(HttpStatus.METHOD_NOT_ALLOWED, "Request method '" + method + "' not supported"); + super(HttpStatus.METHOD_NOT_ALLOWED, "Request method '" + method + "' is not supported."); Assert.notNull(method, "'method' is required"); if (supportedMethods == null) { supportedMethods = Collections.emptySet(); } this.method = method; this.httpMethods = Collections.unmodifiableSet(new LinkedHashSet<>(supportedMethods)); + + getBody().setDetail(this.httpMethods.isEmpty() ? getReason() : + "Supported methods: " + this.httpMethods.stream() + .map(HttpMethod::toString).collect(Collectors.joining("', '", "'", "'"))); } diff --git a/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java b/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java index 625292ab690..212ab3e73f5 100644 --- a/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java @@ -18,6 +18,7 @@ package org.springframework.web.server; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -42,14 +43,17 @@ public class NotAcceptableStatusException extends ResponseStatusException { public NotAcceptableStatusException(String reason) { super(HttpStatus.NOT_ACCEPTABLE, reason); this.supportedMediaTypes = Collections.emptyList(); + getBody().setDetail("Could not parse Accept header."); } /** * Constructor for when the requested Content-Type is not supported. */ - public NotAcceptableStatusException(List supportedMediaTypes) { + public NotAcceptableStatusException(List mediaTypes) { super(HttpStatus.NOT_ACCEPTABLE, "Could not find acceptable representation"); - this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes); + this.supportedMediaTypes = Collections.unmodifiableList(mediaTypes); + getBody().setDetail("Acceptable representations: " + + mediaTypes.stream().map(MediaType::toString).collect(Collectors.joining(", ", "'", "'")) + "."); } diff --git a/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java b/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java index 2739c778771..86d07cb0863 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/ResponseStatusException.java @@ -77,7 +77,6 @@ public class ResponseStatusException extends ErrorResponseException { public ResponseStatusException(int rawStatusCode, @Nullable String reason, @Nullable Throwable cause) { super(rawStatusCode, cause); this.reason = reason; - setDetail(reason); } diff --git a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java index 991a7a69f4c..c39ef1678b4 100644 --- a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java @@ -57,6 +57,7 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException this.supportedMediaTypes = Collections.emptyList(); this.bodyType = null; this.method = null; + getBody().setDetail("Could not parse Content-Type."); } /** @@ -100,8 +101,7 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException this.bodyType = bodyType; this.method = method; - // Set explicitly to avoid implementation details - setDetail(contentType != null ? "Content-Type '" + contentType + "' is not supported" : null); + setDetail(contentType != null ? "Content-Type '" + contentType + "' is not supported." : null); } diff --git a/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java b/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java new file mode 100644 index 00000000000..949fac4a6d1 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java @@ -0,0 +1,349 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ProblemDetail; +import org.springframework.lang.Nullable; +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; +import org.springframework.web.bind.MissingRequestCookieException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.UnsatisfiedServletRequestParameterException; +import org.springframework.web.bind.support.WebExchangeBindException; +import org.springframework.web.context.request.async.AsyncRequestTimeoutException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.server.MethodNotAllowedException; +import org.springframework.web.server.NotAcceptableStatusException; +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. + * + * @author Rossen Stoyanchev + */ +public class ErrorResponseExceptionTests { + + private final MethodParameter methodParameter = + new MethodParameter(ResolvableMethod.on(getClass()).resolveMethod("handle"), 0); + + + @Test + void httpMediaTypeNotSupportedException() { + + List mediaTypes = + Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); + + ErrorResponse 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."); + + HttpHeaders headers = ex.getHeaders(); + assertThat(headers.getAccept()).isEqualTo(mediaTypes); + assertThat(headers.getAcceptPatch()).isEqualTo(mediaTypes); + } + + @Test + void httpMediaTypeNotSupportedExceptionWithParseError() { + + 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."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void httpMediaTypeNotAcceptableException() { + + List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); + ErrorResponse ex = new HttpMediaTypeNotAcceptableException(mediaTypes); + + + assertStatus(ex, HttpStatus.NOT_ACCEPTABLE); + assertDetail(ex, "Acceptable representations: 'application/json, application/cbor'."); + + assertThat(ex.getHeaders()).hasSize(1); + assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes); + } + + @Test + void httpMediaTypeNotAcceptableExceptionWithParseError() { + + 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."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void asyncRequestTimeoutException() { + + ErrorResponse ex = new AsyncRequestTimeoutException(); + + + 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"); + + + assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED); + assertDetail(ex, "Method 'PUT' is not supported."); + + assertThat(ex.getHeaders()).hasSize(1); + assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST); + } + + @Test + void missingRequestHeaderException() { + + ErrorResponse ex = new MissingRequestHeaderException("Authorization", this.methodParameter); + + + assertStatus(ex, HttpStatus.BAD_REQUEST); + assertDetail(ex, "Required header 'Authorization' is not present."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void missingServletRequestParameterException() { + + ErrorResponse ex = new MissingServletRequestParameterException("query", "String"); + + + assertStatus(ex, HttpStatus.BAD_REQUEST); + assertDetail(ex, "Required parameter 'query' is not present."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void missingMatrixVariableException() { + + ErrorResponse ex = new MissingMatrixVariableException("region", this.methodParameter); + + + assertStatus(ex, HttpStatus.BAD_REQUEST); + assertDetail(ex, "Required path parameter 'region' is not present."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void missingPathVariableException() { + + ErrorResponse ex = new MissingPathVariableException("id", this.methodParameter); + + + assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR); + assertDetail(ex, "Required path variable 'id' is not present."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void missingRequestCookieException() { + + ErrorResponse ex = new MissingRequestCookieException("oreo", this.methodParameter); + + + assertStatus(ex, HttpStatus.BAD_REQUEST); + assertDetail(ex, "Required cookie 'oreo' is not present."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void unsatisfiedServletRequestParameterException() { + + ErrorResponse 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."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void missingServletRequestPartException() { + + ErrorResponse ex = new MissingServletRequestPartException("file"); + + assertStatus(ex, HttpStatus.BAD_REQUEST); + assertDetail(ex, "Required part 'file' is not present."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void methodArgumentNotValidException() { + + BindingResult bindingResult = new BindException(new Object(), "object"); + bindingResult.addError(new FieldError("object", "field", "message")); + + ErrorResponse ex = new MethodArgumentNotValidException(this.methodParameter, bindingResult); + + assertStatus(ex, HttpStatus.BAD_REQUEST); + assertDetail(ex, "Invalid request content."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void unsupportedMediaTypeStatusException() { + + List mediaTypes = + Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); + + ErrorResponse ex = new UnsupportedMediaTypeStatusException( + MediaType.APPLICATION_XML, mediaTypes, HttpMethod.PATCH); + + assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE); + assertDetail(ex, "Content-Type 'application/xml' is not supported."); + + HttpHeaders headers = ex.getHeaders(); + assertThat(headers.getAccept()).isEqualTo(mediaTypes); + assertThat(headers.getAcceptPatch()).isEqualTo(mediaTypes); + } + + @Test + void unsupportedMediaTypeStatusExceptionWithParseError() { + + ErrorResponse ex = new UnsupportedMediaTypeStatusException( + "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."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void notAcceptableStatusException() { + + List mediaTypes = + Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); + + ErrorResponse ex = new NotAcceptableStatusException(mediaTypes); + + assertStatus(ex, HttpStatus.NOT_ACCEPTABLE); + assertDetail(ex, "Acceptable representations: 'application/json, application/cbor'."); + + assertThat(ex.getHeaders()).hasSize(1); + assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes); + } + + @Test + void notAcceptableStatusExceptionWithParseError() { + + 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."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void webExchangeBindException() { + + BindingResult bindingResult = new BindException(new Object(), "object"); + bindingResult.addError(new FieldError("object", "field", "message")); + + ErrorResponse ex = new WebExchangeBindException(this.methodParameter, bindingResult); + + assertStatus(ex, HttpStatus.BAD_REQUEST); + assertDetail(ex, "Invalid request content."); + assertThat(ex.getHeaders()).isEmpty(); + } + + @Test + void methodNotAllowedException() { + + List supportedMethods = Arrays.asList(HttpMethod.GET, HttpMethod.POST); + ErrorResponse ex = new MethodNotAllowedException(HttpMethod.PUT, supportedMethods); + + + assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED); + assertDetail(ex, "Supported methods: 'GET', 'POST'"); + + assertThat(ex.getHeaders()).hasSize(1); + assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST); + } + + @Test + void methodNotAllowedExceptionWithoutSupportedMethods() { + + ErrorResponse ex = new MethodNotAllowedException(HttpMethod.PUT, Collections.emptyList()); + + + assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED); + assertDetail(ex, "Request method 'PUT' is not supported."); + assertThat(ex.getHeaders()).isEmpty(); + } + + private void assertStatus(ErrorResponse ex, HttpStatus status) { + ProblemDetail body = ex.getBody(); + assertThat(ex.getStatus()).isEqualTo(status); + assertThat(body.getStatus()).isEqualTo(status.value()); + assertThat(body.getTitle()).isEqualTo(status.getReasonPhrase()); + } + + private void assertDetail(ErrorResponse ex, @Nullable String detail) { + if (detail != null) { + assertThat(ex.getBody().getDetail()).isEqualTo(detail); + } + else { + assertThat(ex.getBody().getDetail()).isNull(); + } + } + + + @SuppressWarnings("unused") + private void handle(String arg) {} + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java index c5a421bb3a0..7673a7b301d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java @@ -45,7 +45,7 @@ public class NoHandlerFoundException extends ServletException implements ErrorRe private final HttpHeaders headers; - private final ProblemDetail detail = ProblemDetail.forRawStatusCode(getRawStatusCode()); + private final ProblemDetail body; /** @@ -55,10 +55,11 @@ public class NoHandlerFoundException extends ServletException implements ErrorRe * @param headers the HTTP request headers */ public NoHandlerFoundException(String httpMethod, String requestURL, HttpHeaders headers) { - super("No handler found for " + httpMethod + " " + requestURL); + super("No endpoint " + httpMethod + " " + requestURL + "."); this.httpMethod = httpMethod; this.requestURL = requestURL; this.headers = headers; + this.body = ProblemDetail.forRawStatusCode(getRawStatusCode()).withDetail(getMessage()); } @@ -81,7 +82,7 @@ public class NoHandlerFoundException extends ServletException implements ErrorRe @Override public ProblemDetail getBody() { - return this.detail; + return this.body; } } 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 268f01548c6..c5ac4972122 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 @@ -154,6 +154,7 @@ public abstract class ResponseEntityExceptionHandler { return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatus(), request); } else { + // Another ErrorResponseException return handleExceptionInternal(ex, null, errorEx.getHeaders(), errorEx.getStatus(), request); } }