Tests for ErrorResponse hierarchy to verify the output
See gh-27052
This commit is contained in:
parent
679432ece6
commit
b045e5baef
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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(", ", "'", "'")) + ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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("', '", "'", "'")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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(", ", "'", "'")) + ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue