Tests for ErrorResponse hierarchy to verify the output

See gh-27052
This commit is contained in:
rstoyanchev 2022-02-28 11:20:07 +00:00
parent 679432ece6
commit b045e5baef
20 changed files with 400 additions and 42 deletions

View File

@ -61,7 +61,7 @@ public class ErrorResponseException extends NestedRuntimeException implements Er
* Constructor with a well-known {@link HttpStatus} and an optional cause. * Constructor with a well-known {@link HttpStatus} and an optional cause.
*/ */
public ErrorResponseException(HttpStatus status, @Nullable Throwable cause) { public ErrorResponseException(HttpStatus status, @Nullable Throwable cause) {
this(status.value(), null); this(status.value(), cause);
} }
/** /**

View File

@ -17,6 +17,7 @@
package org.springframework.web; package org.springframework.web;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -39,15 +40,17 @@ public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException
*/ */
public HttpMediaTypeNotAcceptableException(String message) { public HttpMediaTypeNotAcceptableException(String message) {
super(message); super(message);
getBody().setDetail("Could not parse Accept header"); getBody().setDetail("Could not parse Accept header.");
} }
/** /**
* Create a new HttpMediaTypeNotSupportedException. * Create a new HttpMediaTypeNotSupportedException.
* @param supportedMediaTypes the list of supported media types * @param mediaTypes the list of supported media types
*/ */
public HttpMediaTypeNotAcceptableException(List<MediaType> supportedMediaTypes) { public HttpMediaTypeNotAcceptableException(List<MediaType> mediaTypes) {
super("No acceptable representation", supportedMediaTypes); super("No acceptable representation", mediaTypes);
getBody().setDetail("Acceptable representations: " +
mediaTypes.stream().map(MediaType::toString).collect(Collectors.joining(", ", "'", "'")) + ".");
} }

View File

@ -51,29 +51,29 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
super(message); super(message);
this.contentType = null; this.contentType = null;
this.httpMethod = null; this.httpMethod = null;
getBody().setDetail("Could not parse Content-Type"); getBody().setDetail("Could not parse Content-Type.");
} }
/** /**
* Create a new HttpMediaTypeNotSupportedException. * Create a new HttpMediaTypeNotSupportedException.
* @param contentType the unsupported content type * @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<MediaType> supportedMediaTypes) { public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType, List<MediaType> mediaTypes) {
this(contentType, supportedMediaTypes, null); this(contentType, mediaTypes, null);
} }
/** /**
* Create a new HttpMediaTypeNotSupportedException. * Create a new HttpMediaTypeNotSupportedException.
* @param contentType the unsupported content type * @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 * @param httpMethod the HTTP method of the request
* @since 6.0 * @since 6.0
*/ */
public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType, public HttpMediaTypeNotSupportedException(
List<MediaType> supportedMediaTypes, @Nullable HttpMethod httpMethod) { @Nullable MediaType contentType, List<MediaType> mediaTypes, @Nullable HttpMethod httpMethod) {
this(contentType, supportedMediaTypes, httpMethod, this(contentType, mediaTypes, httpMethod,
"Content-Type " + (contentType != null ? "'" + contentType + "' " : "") + "is not supported"); "Content-Type " + (contentType != null ? "'" + contentType + "' " : "") + "is not supported");
} }
@ -91,7 +91,7 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
super(message, supportedMediaTypes); super(message, supportedMediaTypes);
this.contentType = contentType; this.contentType = contentType;
this.httpMethod = httpMethod; this.httpMethod = httpMethod;
getBody().setDetail("Content-Type " + this.contentType + " is not supported"); getBody().setDetail("Content-Type '" + this.contentType + "' is not supported.");
} }

View File

@ -93,8 +93,9 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
super(msg); super(msg);
this.method = method; this.method = method;
this.supportedMethods = supportedMethods; 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);
} }

View File

@ -48,7 +48,7 @@ public class MethodArgumentNotValidException extends BindException implements Er
public MethodArgumentNotValidException(MethodParameter parameter, BindingResult bindingResult) { public MethodArgumentNotValidException(MethodParameter parameter, BindingResult bindingResult) {
super(bindingResult); super(bindingResult);
this.parameter = parameter; 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 @Override
public String getMessage() { public String getMessage() {
return initMessage(this.parameter);
}
private String initMessage(MethodParameter parameter) {
StringBuilder sb = new StringBuilder("Validation failed for argument [") StringBuilder sb = new StringBuilder("Validation failed for argument [")
.append(parameter.getParameterIndex()).append("] in ") .append(this.parameter.getParameterIndex()).append("] in ")
.append(parameter.getExecutable().toGenericString()); .append(this.parameter.getExecutable().toGenericString());
BindingResult bindingResult = getBindingResult(); BindingResult bindingResult = getBindingResult();
if (bindingResult.getErrorCount() > 1) { if (bindingResult.getErrorCount() > 1) {
sb.append(" with ").append(bindingResult.getErrorCount()).append(" errors"); sb.append(" with ").append(bindingResult.getErrorCount()).append(" errors");

View File

@ -57,7 +57,7 @@ public class MissingMatrixVariableException extends MissingRequestValueException
super("", missingAfterConversion); super("", missingAfterConversion);
this.variableName = variableName; this.variableName = variableName;
this.parameter = parameter; this.parameter = parameter;
getBody().setDetail("Required path parameter '" + this.variableName + "' is not present"); getBody().setDetail("Required path parameter '" + this.variableName + "' is not present.");
} }

View File

@ -60,7 +60,7 @@ public class MissingPathVariableException extends MissingRequestValueException {
super("", missingAfterConversion); super("", missingAfterConversion);
this.variableName = variableName; this.variableName = variableName;
this.parameter = parameter; this.parameter = parameter;
getBody().setDetail("Required URI variable '" + this.variableName + "' is not present"); getBody().setDetail("Required path variable '" + this.variableName + "' is not present.");
} }

View File

@ -57,7 +57,7 @@ public class MissingRequestCookieException extends MissingRequestValueException
super("", missingAfterConversion); super("", missingAfterConversion);
this.cookieName = cookieName; this.cookieName = cookieName;
this.parameter = parameter; this.parameter = parameter;
getBody().setDetail("Required cookie '" + this.cookieName + "' is not present"); getBody().setDetail("Required cookie '" + this.cookieName + "' is not present.");
} }

View File

@ -57,7 +57,7 @@ public class MissingRequestHeaderException extends MissingRequestValueException
super("", missingAfterConversion); super("", missingAfterConversion);
this.headerName = headerName; this.headerName = headerName;
this.parameter = parameter; this.parameter = parameter;
getBody().setDetail("Required header '" + this.headerName + "' is not present"); getBody().setDetail("Required header '" + this.headerName + "' is not present.");
} }

View File

@ -52,7 +52,7 @@ public class MissingServletRequestParameterException extends MissingRequestValue
super("", missingAfterConversion); super("", missingAfterConversion);
this.parameterName = parameterName; this.parameterName = parameterName;
this.parameterType = parameterType; this.parameterType = parameterType;
getBody().setDetail("Required parameter '" + this.parameterName + "' is not present"); getBody().setDetail("Required parameter '" + this.parameterName + "' is not present.");
} }

View File

@ -16,7 +16,6 @@
package org.springframework.web.bind; package org.springframework.web.bind;
import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -48,9 +47,7 @@ public class UnsatisfiedServletRequestParameterException extends ServletRequestB
* @param actualParams the actual parameter Map associated with the ServletRequest * @param actualParams the actual parameter Map associated with the ServletRequest
*/ */
public UnsatisfiedServletRequestParameterException(String[] paramConditions, Map<String, String[]> actualParams) { public UnsatisfiedServletRequestParameterException(String[] paramConditions, Map<String, String[]> actualParams) {
super(""); this(List.<String[]>of(paramConditions), actualParams);
this.paramConditions = Arrays.<String[]>asList(paramConditions);
this.actualParams = actualParams;
} }
/** /**
@ -66,6 +63,7 @@ public class UnsatisfiedServletRequestParameterException extends ServletRequestB
Assert.notEmpty(paramConditions, "Parameter conditions must not be empty"); Assert.notEmpty(paramConditions, "Parameter conditions must not be empty");
this.paramConditions = paramConditions; this.paramConditions = paramConditions;
this.actualParams = actualParams; this.actualParams = actualParams;
getBody().setDetail("Invalid request parameters.");
} }

View File

@ -49,6 +49,7 @@ public class WebExchangeBindException extends ServerWebInputException implements
public WebExchangeBindException(MethodParameter parameter, BindingResult bindingResult) { public WebExchangeBindException(MethodParameter parameter, BindingResult bindingResult) {
super("Validation failure", parameter); super("Validation failure", parameter);
this.bindingResult = bindingResult; this.bindingResult = bindingResult;
getBody().setDetail("Invalid request content.");
} }

View File

@ -40,7 +40,7 @@ public class MissingServletRequestPartException extends ServletRequestBindingExc
* @param requestPartName the name of the missing part of the multipart request * @param requestPartName the name of the missing part of the multipart request
*/ */
public MissingServletRequestPartException(String requestPartName) { public MissingServletRequestPartException(String requestPartName) {
super("Required request part '" + requestPartName + "' is not present"); super("Required part '" + requestPartName + "' is not present.");
this.requestPartName = requestPartName; this.requestPartName = requestPartName;
getBody().setDetail(getMessage()); getBody().setDetail(getMessage());
} }

View File

@ -20,6 +20,7 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -47,13 +48,17 @@ public class MethodNotAllowedException extends ResponseStatusException {
} }
public MethodNotAllowedException(String method, @Nullable Collection<HttpMethod> supportedMethods) { public MethodNotAllowedException(String method, @Nullable Collection<HttpMethod> 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"); Assert.notNull(method, "'method' is required");
if (supportedMethods == null) { if (supportedMethods == null) {
supportedMethods = Collections.emptySet(); supportedMethods = Collections.emptySet();
} }
this.method = method; this.method = method;
this.httpMethods = Collections.unmodifiableSet(new LinkedHashSet<>(supportedMethods)); this.httpMethods = Collections.unmodifiableSet(new LinkedHashSet<>(supportedMethods));
getBody().setDetail(this.httpMethods.isEmpty() ? getReason() :
"Supported methods: " + this.httpMethods.stream()
.map(HttpMethod::toString).collect(Collectors.joining("', '", "'", "'")));
} }

View File

@ -18,6 +18,7 @@ package org.springframework.web.server;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -42,14 +43,17 @@ public class NotAcceptableStatusException extends ResponseStatusException {
public NotAcceptableStatusException(String reason) { public NotAcceptableStatusException(String reason) {
super(HttpStatus.NOT_ACCEPTABLE, reason); super(HttpStatus.NOT_ACCEPTABLE, reason);
this.supportedMediaTypes = Collections.emptyList(); this.supportedMediaTypes = Collections.emptyList();
getBody().setDetail("Could not parse Accept header.");
} }
/** /**
* Constructor for when the requested Content-Type is not supported. * Constructor for when the requested Content-Type is not supported.
*/ */
public NotAcceptableStatusException(List<MediaType> supportedMediaTypes) { public NotAcceptableStatusException(List<MediaType> mediaTypes) {
super(HttpStatus.NOT_ACCEPTABLE, "Could not find acceptable representation"); 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(", ", "'", "'")) + ".");
} }

View File

@ -77,7 +77,6 @@ public class ResponseStatusException extends ErrorResponseException {
public ResponseStatusException(int rawStatusCode, @Nullable String reason, @Nullable Throwable cause) { public ResponseStatusException(int rawStatusCode, @Nullable String reason, @Nullable Throwable cause) {
super(rawStatusCode, cause); super(rawStatusCode, cause);
this.reason = reason; this.reason = reason;
setDetail(reason);
} }

View File

@ -57,6 +57,7 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
this.supportedMediaTypes = Collections.emptyList(); this.supportedMediaTypes = Collections.emptyList();
this.bodyType = null; this.bodyType = null;
this.method = null; this.method = null;
getBody().setDetail("Could not parse Content-Type.");
} }
/** /**
@ -100,8 +101,7 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
this.bodyType = bodyType; this.bodyType = bodyType;
this.method = method; 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);
} }

View File

@ -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<MediaType> 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<MediaType> 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<MediaType> 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<MediaType> 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<HttpMethod> 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) {}
}

View File

@ -45,7 +45,7 @@ public class NoHandlerFoundException extends ServletException implements ErrorRe
private final HttpHeaders headers; 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 * @param headers the HTTP request headers
*/ */
public NoHandlerFoundException(String httpMethod, String requestURL, HttpHeaders 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.httpMethod = httpMethod;
this.requestURL = requestURL; this.requestURL = requestURL;
this.headers = headers; this.headers = headers;
this.body = ProblemDetail.forRawStatusCode(getRawStatusCode()).withDetail(getMessage());
} }
@ -81,7 +82,7 @@ public class NoHandlerFoundException extends ServletException implements ErrorRe
@Override @Override
public ProblemDetail getBody() { public ProblemDetail getBody() {
return this.detail; return this.body;
} }
} }

View File

@ -154,6 +154,7 @@ public abstract class ResponseEntityExceptionHandler {
return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatus(), request); return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatus(), request);
} }
else { else {
// Another ErrorResponseException
return handleExceptionInternal(ex, null, errorEx.getHeaders(), errorEx.getStatus(), request); return handleExceptionInternal(ex, null, errorEx.getHeaders(), errorEx.getStatus(), request);
} }
} }