Skip to content

Commit 3efedef

Browse files
committed
Add ErrorResponse and ErrorResponseException
ErrorResponse represents a complete error response with status, headers, and an RFC 7807 ProblemDetail body. ErrorResponseException implements ErrorResponse and is usable on its own or as a base class. ResponseStatusException extends ErrorResponseException and now also supports RFC 7807 and so does its sub-hierarchy. ErrorResponse can be returned from `@ExceptionHandler` methods and is mapped to ResponseEntity. See gh-27052
1 parent 714d451 commit 3efedef

File tree

13 files changed

+406
-81
lines changed

13 files changed

+406
-81
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web;
18+
19+
import org.springframework.http.HttpHeaders;
20+
import org.springframework.http.HttpStatus;
21+
import org.springframework.http.ProblemDetail;
22+
23+
24+
/**
25+
* Representation of a complete RFC 7807 error response including status,
26+
* headers, and an RFC 7808 formatted {@link ProblemDetail} body. Allows any
27+
* exception to expose HTTP error response information.
28+
*
29+
* <p>{@link ErrorResponseException} is a default implementation of this
30+
* interface and a convenient base class for other exceptions to use.
31+
*
32+
* <p>An {@code @ExceptionHandler} method can use
33+
* {@link org.springframework.http.ResponseEntity#of(ErrorResponse)} to map an
34+
* {@code ErrorResponse} to a {@code ResponseEntity}.
35+
*
36+
* @author Rossen Stoyanchev
37+
* @since 6.0
38+
* @see ErrorResponseException
39+
* @see org.springframework.http.ResponseEntity#of(ErrorResponse)
40+
*/
41+
public interface ErrorResponse {
42+
43+
/**
44+
* Return the HTTP status to use for the response.
45+
* @throws IllegalArgumentException for an unknown HTTP status code
46+
*/
47+
default HttpStatus getStatus() {
48+
return HttpStatus.valueOf(getRawStatusCode());
49+
}
50+
51+
/**
52+
* Return the HTTP status value for the response, potentially non-standard
53+
* and not resolvable via {@link HttpStatus}.
54+
*/
55+
int getRawStatusCode();
56+
57+
/**
58+
* Return headers to use for the response.
59+
*/
60+
default HttpHeaders getHeaders() {
61+
return HttpHeaders.EMPTY;
62+
}
63+
64+
/**
65+
* Return the body for the response, formatted as an RFC 7807
66+
* {@link ProblemDetail} whose {@link ProblemDetail#getStatus() status}
67+
* should match the response status.
68+
*/
69+
ProblemDetail getBody();
70+
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web;
18+
19+
20+
import java.net.URI;
21+
22+
import org.springframework.core.NestedExceptionUtils;
23+
import org.springframework.core.NestedRuntimeException;
24+
import org.springframework.http.HttpHeaders;
25+
import org.springframework.http.HttpStatus;
26+
import org.springframework.http.ProblemDetail;
27+
import org.springframework.lang.Nullable;
28+
29+
30+
/**
31+
* {@link RuntimeException} that implements {@link ErrorResponse} to expose
32+
* an HTTP status, response headers, and a body formatted as an RFC 7808
33+
* {@link ProblemDetail}.
34+
*
35+
* <p>The exception can be used as is, or it can be extended as a more specific
36+
* exception that populates the {@link ProblemDetail#setType(URI) type} or
37+
* {@link ProblemDetail#setDetail(String) detail} fields, or potentially adds
38+
* other non-standard fields.
39+
*
40+
* @author Rossen Stoyanchev
41+
* @since 6.0
42+
*/
43+
@SuppressWarnings("serial")
44+
public class ErrorResponseException extends NestedRuntimeException implements ErrorResponse {
45+
46+
private final int status;
47+
48+
private final HttpHeaders headers = new HttpHeaders();
49+
50+
private final ProblemDetail body;
51+
52+
53+
/**
54+
* Constructor with a well-known {@link HttpStatus}.
55+
*/
56+
public ErrorResponseException(HttpStatus status) {
57+
this(status, null);
58+
}
59+
60+
/**
61+
* Constructor with a well-known {@link HttpStatus} and an optional cause.
62+
*/
63+
public ErrorResponseException(HttpStatus status, @Nullable Throwable cause) {
64+
this(status.value(), null);
65+
}
66+
67+
/**
68+
* Constructor that accepts any status value, possibly not resolvable as an
69+
* {@link HttpStatus} enum, and an optional cause.
70+
*/
71+
public ErrorResponseException(int status, @Nullable Throwable cause) {
72+
this(status, ProblemDetail.forRawStatusCode(status), cause);
73+
}
74+
75+
/**
76+
* Constructor with a given {@link ProblemDetail} instance, possibly a
77+
* subclass of {@code ProblemDetail} with extended fields.
78+
*/
79+
public ErrorResponseException(int status, ProblemDetail body, @Nullable Throwable cause) {
80+
super(null, cause);
81+
this.status = status;
82+
this.body = body;
83+
}
84+
85+
86+
@Override
87+
public int getRawStatusCode() {
88+
return this.status;
89+
}
90+
91+
@Override
92+
public HttpHeaders getHeaders() {
93+
return this.headers;
94+
}
95+
96+
/**
97+
* Set the {@link ProblemDetail#setType(URI) type} field of the response body.
98+
* @param type the problem type
99+
*/
100+
public void setType(URI type) {
101+
this.body.setType(type);
102+
}
103+
104+
/**
105+
* Set the {@link ProblemDetail#setTitle(String) title} field of the response body.
106+
* @param title the problem title
107+
*/
108+
public void setTitle(@Nullable String title) {
109+
this.body.setTitle(title);
110+
}
111+
112+
/**
113+
* Set the {@link ProblemDetail#setDetail(String) detail} field of the response body.
114+
* @param detail the problem detail
115+
*/
116+
public void setDetail(@Nullable String detail) {
117+
this.body.setDetail(detail);
118+
}
119+
120+
/**
121+
* Set the {@link ProblemDetail#setInstance(URI) instance} field of the response body.
122+
* @param instance the problem instance
123+
*/
124+
public void setInstance(@Nullable URI instance) {
125+
this.body.setInstance(instance);
126+
}
127+
128+
/**
129+
* Return the body for the response. To customize the body content, use:
130+
* <ul>
131+
* <li>{@link #setType(URI)}
132+
* <li>{@link #setTitle(String)}
133+
* <li>{@link #setDetail(String)}
134+
* <li>{@link #setInstance(URI)}
135+
* </ul>
136+
* <p>By default, the status field of {@link ProblemDetail} is initialized
137+
* from the status provided to the constructor, which in turn may also
138+
* initialize the title field from the status reason phrase, if the status
139+
* is well-known. The instance field, if not set, is initialized from the
140+
* request path when a {@code ProblemDetail} is returned from an
141+
* {@code @ExceptionHandler} method.
142+
*/
143+
@Override
144+
public final ProblemDetail getBody() {
145+
return this.body;
146+
}
147+
148+
@Override
149+
public String getMessage() {
150+
HttpStatus httpStatus = HttpStatus.resolve(this.status);
151+
String message = (httpStatus != null ? httpStatus : String.valueOf(this.status)) +
152+
(!this.headers.isEmpty() ? ", headers=" + this.headers : "") + ", " + this.body;
153+
return NestedExceptionUtils.buildMessage(message, getCause());
154+
}
155+
156+
}

spring-web/src/main/java/org/springframework/web/server/MethodNotAllowedException.java

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -58,11 +58,11 @@ public MethodNotAllowedException(String method, @Nullable Collection<HttpMethod>
5858

5959

6060
/**
61-
* Return HttpHeaders with an "Allow" header.
62-
* @since 5.1.13
61+
* Return HttpHeaders with an "Allow" header that documents the allowed
62+
* HTTP methods for this URL, if available, or an empty instance otherwise.
6363
*/
6464
@Override
65-
public HttpHeaders getResponseHeaders() {
65+
public HttpHeaders getHeaders() {
6666
if (CollectionUtils.isEmpty(this.httpMethods)) {
6767
return HttpHeaders.EMPTY;
6868
}
@@ -71,6 +71,17 @@ public HttpHeaders getResponseHeaders() {
7171
return headers;
7272
}
7373

74+
/**
75+
* Delegates to {@link #getHeaders()}.
76+
* @since 5.1.13
77+
* @deprecated as of 6.0 in favor of {@link #getHeaders()}
78+
*/
79+
@Deprecated
80+
@Override
81+
public HttpHeaders getResponseHeaders() {
82+
return getHeaders();
83+
}
84+
7485
/**
7586
* Return the HTTP method for the failed request.
7687
*/

spring-web/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -54,11 +54,11 @@ public NotAcceptableStatusException(List<MediaType> supportedMediaTypes) {
5454

5555

5656
/**
57-
* Return HttpHeaders with an "Accept" header, or an empty instance.
58-
* @since 5.1.13
57+
* Return HttpHeaders with an "Accept" header that documents the supported
58+
* media types, if available, or an empty instance otherwise.
5959
*/
6060
@Override
61-
public HttpHeaders getResponseHeaders() {
61+
public HttpHeaders getHeaders() {
6262
if (CollectionUtils.isEmpty(this.supportedMediaTypes)) {
6363
return HttpHeaders.EMPTY;
6464
}
@@ -67,6 +67,17 @@ public HttpHeaders getResponseHeaders() {
6767
return headers;
6868
}
6969

70+
/**
71+
* Delegates to {@link #getHeaders()}.
72+
* @since 5.1.13
73+
* @deprecated as of 6.0 in favor of {@link #getHeaders()}
74+
*/
75+
@Deprecated
76+
@Override
77+
public HttpHeaders getResponseHeaders() {
78+
return getHeaders();
79+
}
80+
7081
/**
7182
* Return the list of supported content types in cases when the Accept
7283
* header is parsed but not supported, or an empty list otherwise.

0 commit comments

Comments
 (0)