diff --git a/spring-web/src/main/java/org/springframework/web/ErrorResponse.java b/spring-web/src/main/java/org/springframework/web/ErrorResponse.java new file mode 100644 index 0000000000..a8527a1852 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/ErrorResponse.java @@ -0,0 +1,71 @@ +/* + * 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 org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; + + +/** + * Representation of a complete RFC 7807 error response including status, + * headers, and an RFC 7808 formatted {@link ProblemDetail} body. Allows any + * exception to expose HTTP error response information. + * + *
{@link ErrorResponseException} is a default implementation of this + * interface and a convenient base class for other exceptions to use. + * + *
An {@code @ExceptionHandler} method can use + * {@link org.springframework.http.ResponseEntity#of(ErrorResponse)} to map an + * {@code ErrorResponse} to a {@code ResponseEntity}. + * + * @author Rossen Stoyanchev + * @since 6.0 + * @see ErrorResponseException + * @see org.springframework.http.ResponseEntity#of(ErrorResponse) + */ +public interface ErrorResponse { + + /** + * Return the HTTP status to use for the response. + * @throws IllegalArgumentException for an unknown HTTP status code + */ + default HttpStatus getStatus() { + return HttpStatus.valueOf(getRawStatusCode()); + } + + /** + * Return the HTTP status value for the response, potentially non-standard + * and not resolvable via {@link HttpStatus}. + */ + int getRawStatusCode(); + + /** + * Return headers to use for the response. + */ + default HttpHeaders getHeaders() { + return HttpHeaders.EMPTY; + } + + /** + * Return the body for the response, formatted as an RFC 7807 + * {@link ProblemDetail} whose {@link ProblemDetail#getStatus() status} + * should match the response status. + */ + ProblemDetail getBody(); + +} diff --git a/spring-web/src/main/java/org/springframework/web/ErrorResponseException.java b/spring-web/src/main/java/org/springframework/web/ErrorResponseException.java new file mode 100644 index 0000000000..ec84ecea2e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/ErrorResponseException.java @@ -0,0 +1,156 @@ +/* + * 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.net.URI; + +import org.springframework.core.NestedExceptionUtils; +import org.springframework.core.NestedRuntimeException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.lang.Nullable; + + +/** + * {@link RuntimeException} that implements {@link ErrorResponse} to expose + * an HTTP status, response headers, and a body formatted as an RFC 7808 + * {@link ProblemDetail}. + * + *
The exception can be used as is, or it can be extended as a more specific + * exception that populates the {@link ProblemDetail#setType(URI) type} or + * {@link ProblemDetail#setDetail(String) detail} fields, or potentially adds + * other non-standard fields. + * + * @author Rossen Stoyanchev + * @since 6.0 + */ +@SuppressWarnings("serial") +public class ErrorResponseException extends NestedRuntimeException implements ErrorResponse { + + private final int status; + + private final HttpHeaders headers = new HttpHeaders(); + + private final ProblemDetail body; + + + /** + * Constructor with a well-known {@link HttpStatus}. + */ + public ErrorResponseException(HttpStatus status) { + this(status, null); + } + + /** + * Constructor with a well-known {@link HttpStatus} and an optional cause. + */ + public ErrorResponseException(HttpStatus status, @Nullable Throwable cause) { + this(status.value(), null); + } + + /** + * Constructor that accepts any status value, possibly not resolvable as an + * {@link HttpStatus} enum, and an optional cause. + */ + public ErrorResponseException(int status, @Nullable Throwable cause) { + this(status, ProblemDetail.forRawStatusCode(status), cause); + } + + /** + * Constructor with a given {@link ProblemDetail} instance, possibly a + * subclass of {@code ProblemDetail} with extended fields. + */ + public ErrorResponseException(int status, ProblemDetail body, @Nullable Throwable cause) { + super(null, cause); + this.status = status; + this.body = body; + } + + + @Override + public int getRawStatusCode() { + return this.status; + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + /** + * Set the {@link ProblemDetail#setType(URI) type} field of the response body. + * @param type the problem type + */ + public void setType(URI type) { + this.body.setType(type); + } + + /** + * Set the {@link ProblemDetail#setTitle(String) title} field of the response body. + * @param title the problem title + */ + public void setTitle(@Nullable String title) { + this.body.setTitle(title); + } + + /** + * Set the {@link ProblemDetail#setDetail(String) detail} field of the response body. + * @param detail the problem detail + */ + public void setDetail(@Nullable String detail) { + this.body.setDetail(detail); + } + + /** + * Set the {@link ProblemDetail#setInstance(URI) instance} field of the response body. + * @param instance the problem instance + */ + public void setInstance(@Nullable URI instance) { + this.body.setInstance(instance); + } + + /** + * Return the body for the response. To customize the body content, use: + *
By default, the status field of {@link ProblemDetail} is initialized + * from the status provided to the constructor, which in turn may also + * initialize the title field from the status reason phrase, if the status + * is well-known. The instance field, if not set, is initialized from the + * request path when a {@code ProblemDetail} is returned from an + * {@code @ExceptionHandler} method. + */ + @Override + public final ProblemDetail getBody() { + return this.body; + } + + @Override + public String getMessage() { + HttpStatus httpStatus = HttpStatus.resolve(this.status); + String message = (httpStatus != null ? httpStatus : String.valueOf(this.status)) + + (!this.headers.isEmpty() ? ", headers=" + this.headers : "") + ", " + this.body; + return NestedExceptionUtils.buildMessage(message, getCause()); + } + +} 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 40f78ed70e..be3271aea0 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * 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. @@ -58,11 +58,11 @@ public class MethodNotAllowedException extends ResponseStatusException { /** - * Return HttpHeaders with an "Allow" header. - * @since 5.1.13 + * Return HttpHeaders with an "Allow" header that documents the allowed + * HTTP methods for this URL, if available, or an empty instance otherwise. */ @Override - public HttpHeaders getResponseHeaders() { + public HttpHeaders getHeaders() { if (CollectionUtils.isEmpty(this.httpMethods)) { return HttpHeaders.EMPTY; } @@ -71,6 +71,17 @@ public class MethodNotAllowedException extends ResponseStatusException { return headers; } + /** + * Delegates to {@link #getHeaders()}. + * @since 5.1.13 + * @deprecated as of 6.0 in favor of {@link #getHeaders()} + */ + @Deprecated + @Override + public HttpHeaders getResponseHeaders() { + return getHeaders(); + } + /** * Return the HTTP method for the failed request. */ 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 7874fe1a2d..625292ab69 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * 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. @@ -54,11 +54,11 @@ public class NotAcceptableStatusException extends ResponseStatusException { /** - * Return HttpHeaders with an "Accept" header, or an empty instance. - * @since 5.1.13 + * Return HttpHeaders with an "Accept" header that documents the supported + * media types, if available, or an empty instance otherwise. */ @Override - public HttpHeaders getResponseHeaders() { + public HttpHeaders getHeaders() { if (CollectionUtils.isEmpty(this.supportedMediaTypes)) { return HttpHeaders.EMPTY; } @@ -67,6 +67,17 @@ public class NotAcceptableStatusException extends ResponseStatusException { return headers; } + /** + * Delegates to {@link #getHeaders()}. + * @since 5.1.13 + * @deprecated as of 6.0 in favor of {@link #getHeaders()} + */ + @Deprecated + @Override + public HttpHeaders getResponseHeaders() { + return getHeaders(); + } + /** * Return the list of supported content types in cases when the Accept * header is parsed but not supported, or an empty list otherwise. 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 9aefb13334..2739c77877 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * 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. @@ -17,23 +17,21 @@ package org.springframework.web.server; import org.springframework.core.NestedExceptionUtils; -import org.springframework.core.NestedRuntimeException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; +import org.springframework.web.ErrorResponseException; /** - * Base class for exceptions associated with specific HTTP response status codes. + * Subclass of {@link ErrorResponseException} that accepts a "reason" and maps + * it to the "detail" property of {@link org.springframework.http.ProblemDetail}. * * @author Rossen Stoyanchev * @author Juergen Hoeller * @since 5.0 */ @SuppressWarnings("serial") -public class ResponseStatusException extends NestedRuntimeException { - - private final int status; +public class ResponseStatusException extends ErrorResponseException { @Nullable private final String reason; @@ -54,10 +52,7 @@ public class ResponseStatusException extends NestedRuntimeException { * @param reason the associated reason (optional) */ public ResponseStatusException(HttpStatus status, @Nullable String reason) { - super(""); - Assert.notNull(status, "HttpStatus is required"); - this.status = status.value(); - this.reason = reason; + this(status, reason, null); } /** @@ -68,10 +63,7 @@ public class ResponseStatusException extends NestedRuntimeException { * @param cause a nested exception (optional) */ public ResponseStatusException(HttpStatus status, @Nullable String reason, @Nullable Throwable cause) { - super(null, cause); - Assert.notNull(status, "HttpStatus is required"); - this.status = status.value(); - this.reason = reason; + this(status.value(), reason, cause); } /** @@ -83,44 +75,12 @@ public class ResponseStatusException extends NestedRuntimeException { * @since 5.3 */ public ResponseStatusException(int rawStatusCode, @Nullable String reason, @Nullable Throwable cause) { - super(null, cause); - this.status = rawStatusCode; + super(rawStatusCode, cause); this.reason = reason; + setDetail(reason); } - /** - * Return the HTTP status associated with this exception. - * @throws IllegalArgumentException in case of an unknown HTTP status code - * @since #getRawStatusCode() - * @see HttpStatus#valueOf(int) - */ - public HttpStatus getStatus() { - return HttpStatus.valueOf(this.status); - } - - /** - * Return the HTTP status code (potentially non-standard and not resolvable - * through the {@link HttpStatus} enum) as an integer. - * @return the HTTP status as an integer value - * @since 5.3 - * @see #getStatus() - * @see HttpStatus#resolve(int) - */ - public int getRawStatusCode() { - return this.status; - } - - /** - * Return headers associated with the exception that should be added to the - * error response, e.g. "Allow", "Accept", etc. - *
The default implementation in this class returns empty headers. - * @since 5.1.13 - */ - public HttpHeaders getResponseHeaders() { - return HttpHeaders.EMPTY; - } - /** * The reason explaining the exception (potentially {@code null} or empty). */ @@ -129,11 +89,32 @@ public class ResponseStatusException extends NestedRuntimeException { return this.reason; } + /** + * Return headers to add to the error response, e.g. "Allow", "Accept", etc. + *
By default, delegates to {@link #getResponseHeaders()} for backwards + * compatibility. + */ + @Override + public HttpHeaders getHeaders() { + return getResponseHeaders(); + } + + /** + * Return headers associated with the exception that should be added to the + * error response, e.g. "Allow", "Accept", etc. + *
The default implementation in this class returns empty headers.
+ * @since 5.1.13
+ * @deprecated as of 6.0 in favor of {@link #getHeaders()}
+ */
+ @Deprecated
+ public HttpHeaders getResponseHeaders() {
+ return HttpHeaders.EMPTY;
+ }
@Override
public String getMessage() {
- HttpStatus code = HttpStatus.resolve(this.status);
- String msg = (code != null ? code : this.status) + (this.reason != null ? " \"" + this.reason + "\"" : "");
+ HttpStatus code = HttpStatus.resolve(getRawStatusCode());
+ String msg = (code != null ? code : getRawStatusCode()) + (this.reason != null ? " \"" + this.reason + "\"" : "");
return NestedExceptionUtils.buildMessage(msg, getCause());
}
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 67c2b63adc..991a7a69f4 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -91,16 +91,17 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
public UnsupportedMediaTypeStatusException(@Nullable MediaType contentType, List By default the order for this result handler is set to 0. It is generally
* safe to place it early in the order as it looks for a concrete return type.
@@ -108,7 +109,8 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
return false;
}
return ((HttpEntity.class.isAssignableFrom(type) && !RequestEntity.class.isAssignableFrom(type)) ||
- HttpHeaders.class.isAssignableFrom(type) || ProblemDetail.class.isAssignableFrom(type));
+ ErrorResponse.class.isAssignableFrom(type) || ProblemDetail.class.isAssignableFrom(type) ||
+ HttpHeaders.class.isAssignableFrom(type));
}
@@ -138,12 +140,15 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
if (returnValue instanceof HttpEntity) {
httpEntity = (HttpEntity>) returnValue;
}
- else if (returnValue instanceof HttpHeaders) {
- httpEntity = new ResponseEntity<>((HttpHeaders) returnValue, HttpStatus.OK);
+ else if (returnValue instanceof ErrorResponse response) {
+ httpEntity = new ResponseEntity<>(response.getBody(), response.getHeaders(), response.getRawStatusCode());
}
else if (returnValue instanceof ProblemDetail detail) {
httpEntity = new ResponseEntity<>(returnValue, HttpHeaders.EMPTY, detail.getStatus());
}
+ else if (returnValue instanceof HttpHeaders) {
+ httpEntity = new ResponseEntity<>((HttpHeaders) returnValue, HttpStatus.OK);
+ }
else {
throw new IllegalArgumentException(
"HttpEntity or HttpHeaders expected but got: " + returnValue.getClass());
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
index 7e13ecf544..e73d2468d8 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -330,7 +330,7 @@ public class RequestMappingInfoHandlerMappingTests {
UnsupportedMediaTypeStatusException umtse = (UnsupportedMediaTypeStatusException) ex;
MediaType mediaType = new MediaType("foo", "bar");
assertThat(umtse.getSupportedMediaTypes()).containsExactly(mediaType);
- assertThat(umtse.getResponseHeaders().getAcceptPatch()).containsExactly(mediaType);
+ assertThat(umtse.getHeaders().getAcceptPatch()).containsExactly(mediaType);
})
.verify();
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
index 34692edd7e..0176e8cd0a 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -55,6 +55,8 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.ObjectUtils;
+import org.springframework.web.ErrorResponse;
+import org.springframework.web.ErrorResponseException;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
@@ -130,6 +132,9 @@ public class ResponseEntityResultHandlerTests {
returnType = on(TestController.class).resolveReturnType(HttpHeaders.class);
assertThat(this.resultHandler.supports(handlerResult(value, returnType))).isTrue();
+ returnType = on(TestController.class).resolveReturnType(ErrorResponse.class);
+ assertThat(this.resultHandler.supports(handlerResult(value, returnType))).isTrue();
+
returnType = on(TestController.class).resolveReturnType(ProblemDetail.class);
assertThat(this.resultHandler.supports(handlerResult(value, returnType))).isTrue();
@@ -236,6 +241,28 @@ public class ResponseEntityResultHandlerTests {
testHandle(returnValue, returnType);
}
+ @Test
+ public void handleErrorResponse() {
+ ErrorResponseException ex = new ErrorResponseException(HttpStatus.BAD_REQUEST);
+ ex.getHeaders().add("foo", "bar");
+ MethodParameter returnType = on(TestController.class).resolveReturnType(ErrorResponse.class);
+ HandlerResult result = handlerResult(ex, returnType);
+ MockServerWebExchange exchange = MockServerWebExchange.from(get("/path"));
+ exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_PROBLEM_JSON);
+ this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5));
+
+ assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(exchange.getResponse().getHeaders()).hasSize(3);
+ assertThat(exchange.getResponse().getHeaders().get("foo")).containsExactly("bar");
+ assertThat(exchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PROBLEM_JSON);
+ assertResponseBody(exchange,
+ "{\"type\":\"about:blank\"," +
+ "\"title\":\"Bad Request\"," +
+ "\"status\":400," +
+ "\"detail\":null," +
+ "\"instance\":\"/path\"}");
+ }
+
@Test
public void handleProblemDetail() {
ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
@@ -529,6 +556,8 @@ public class ResponseEntityResultHandlerTests {
ResponseEntity The default implementation applies the headers from
- * {@link ResponseStatusException#getResponseHeaders()} and delegates to
+ * {@link ResponseStatusException#getHeaders()} and delegates to
* {@link #applyStatusAndReason} with the status code and reason from the
* exception.
* @param ex the exception
@@ -130,9 +130,7 @@ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionRes
protected ModelAndView resolveResponseStatusException(ResponseStatusException ex,
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws Exception {
- ex.getResponseHeaders().forEach((name, values) ->
- values.forEach(value -> response.addHeader(name, value)));
-
+ ex.getHeaders().forEach((name, values) -> values.forEach(value -> response.addHeader(name, value)));
return applyStatusAndReason(ex.getRawStatusCode(), ex.getReason(), response);
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
index 154fc49a3b..b194b5dcf7 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
@@ -44,6 +44,7 @@ import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
+import org.springframework.web.ErrorResponse;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -56,7 +57,7 @@ import org.springframework.web.servlet.support.RequestContextUtils;
/**
* Resolves {@link HttpEntity} and {@link RequestEntity} method argument values,
* as well as return values of type {@link HttpEntity}, {@link ResponseEntity},
- * and {@link ProblemDetail}.
+ * {@link ErrorResponse} and {@link ProblemDetail}.
*
* An {@link HttpEntity} return type has a specific purpose. Therefore, this
* handler should be configured ahead of handlers that support any return
@@ -122,7 +123,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
public boolean supportsReturnType(MethodParameter returnType) {
Class> type = returnType.getParameterType();
return ((HttpEntity.class.isAssignableFrom(type) && !RequestEntity.class.isAssignableFrom(type)) ||
- ProblemDetail.class.isAssignableFrom(type));
+ ErrorResponse.class.isAssignableFrom(type) || ProblemDetail.class.isAssignableFrom(type));
}
@Override
@@ -180,7 +181,10 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
HttpEntity> httpEntity;
- if (returnValue instanceof ProblemDetail detail) {
+ if (returnValue instanceof ErrorResponse response) {
+ httpEntity = new ResponseEntity<>(response.getBody(), response.getHeaders(), response.getRawStatusCode());
+ }
+ else if (returnValue instanceof ProblemDetail detail) {
httpEntity = new ResponseEntity<>(returnValue, HttpHeaders.EMPTY, detail.getStatus());
}
else {
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java
index 0410cff0a7..11b54eb828 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java
@@ -52,6 +52,8 @@ import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.web.ErrorResponse;
+import org.springframework.web.ErrorResponseException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -129,6 +131,8 @@ public class HttpEntityMethodProcessorMockTests {
private MethodParameter returnTypeInt;
+ private MethodParameter returnTypeErrorResponse;
+
private MethodParameter returnTypeProblemDetail;
private ModelAndViewContainer mavContainer;
@@ -175,7 +179,8 @@ public class HttpEntityMethodProcessorMockTests {
returnTypeHttpEntitySubclass = new MethodParameter(getClass().getMethod("handle2x", HttpEntity.class), -1);
returnTypeInt = new MethodParameter(getClass().getMethod("handle3"), -1);
returnTypeResponseEntityResource = new MethodParameter(getClass().getMethod("handle5"), -1);
- returnTypeProblemDetail = new MethodParameter(getClass().getMethod("handle6"), -1);
+ returnTypeErrorResponse = new MethodParameter(getClass().getMethod("handle6"), -1);
+ returnTypeProblemDetail = new MethodParameter(getClass().getMethod("handle7"), -1);
mavContainer = new ModelAndViewContainer();
servletRequest = new MockHttpServletRequest("GET", "/foo");
@@ -197,6 +202,7 @@ public class HttpEntityMethodProcessorMockTests {
assertThat(processor.supportsReturnType(returnTypeResponseEntity)).as("ResponseEntity return type not supported").isTrue();
assertThat(processor.supportsReturnType(returnTypeHttpEntity)).as("HttpEntity return type not supported").isTrue();
assertThat(processor.supportsReturnType(returnTypeHttpEntitySubclass)).as("Custom HttpEntity subclass not supported").isTrue();
+ assertThat(processor.supportsReturnType(returnTypeErrorResponse)).isTrue();
assertThat(processor.supportsReturnType(returnTypeProblemDetail)).isTrue();
assertThat(processor.supportsReturnType(paramRequestEntity)).as("RequestEntity parameter supported").isFalse();
assertThat(processor.supportsReturnType(returnTypeInt)).as("non-ResponseBody return type supported").isFalse();
@@ -282,6 +288,37 @@ public class HttpEntityMethodProcessorMockTests {
verify(stringHttpMessageConverter).write(eq(body), eq(accepted), isA(HttpOutputMessage.class));
}
+ @Test
+ public void shouldHandleErrorResponse() throws Exception {
+ ErrorResponseException ex = new ErrorResponseException(HttpStatus.BAD_REQUEST);
+ ex.getHeaders().add("foo", "bar");
+ servletRequest.addHeader("Accept", APPLICATION_PROBLEM_JSON_VALUE);
+ given(jsonMessageConverter.canWrite(ProblemDetail.class, APPLICATION_PROBLEM_JSON)).willReturn(true);
+
+ processor.handleReturnValue(ex, returnTypeProblemDetail, mavContainer, webRequest);
+
+ assertThat(mavContainer.isRequestHandled()).isTrue();
+ assertThat(webRequest.getNativeResponse(HttpServletResponse.class).getStatus()).isEqualTo(400);
+ verify(jsonMessageConverter).write(eq(ex.getBody()), eq(APPLICATION_PROBLEM_JSON), isA(HttpOutputMessage.class));
+
+ assertThat(ex.getBody()).isNotNull()
+ .extracting(ProblemDetail::getInstance).isNotNull()
+ .extracting(URI::toString)
+ .as("Instance was not set to the request path")
+ .isEqualTo(servletRequest.getRequestURI());
+
+
+ // But if instance is set, it should be respected
+ ex.getBody().setInstance(URI.create("/something/else"));
+ processor.handleReturnValue(ex, returnTypeProblemDetail, mavContainer, webRequest);
+
+ assertThat(ex.getBody()).isNotNull()
+ .extracting(ProblemDetail::getInstance).isNotNull()
+ .extracting(URI::toString)
+ .as("Instance was not set to the request path")
+ .isEqualTo("/something/else");
+ }
+
@Test
public void shouldHandleProblemDetail() throws Exception {
ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
@@ -294,7 +331,7 @@ public class HttpEntityMethodProcessorMockTests {
assertThat(webRequest.getNativeResponse(HttpServletResponse.class).getStatus()).isEqualTo(400);
verify(jsonMessageConverter).write(eq(problemDetail), eq(APPLICATION_PROBLEM_JSON), isA(HttpOutputMessage.class));
- assertThat(problemDetail).isNotNull()
+ assertThat(problemDetail)
.extracting(ProblemDetail::getInstance).isNotNull()
.extracting(URI::toString)
.as("Instance was not set to the request path")
@@ -842,7 +879,12 @@ public class HttpEntityMethodProcessorMockTests {
}
@SuppressWarnings("unused")
- public ProblemDetail handle6() {
+ public ErrorResponse handle6() {
+ return null;
+ }
+
+ @SuppressWarnings("unused")
+ public ProblemDetail handle7() {
return null;
}