Skip to content

Commit 0348a7b

Browse files
committed
Improve API for RFC 7807 in functional endpoints
Closes gh-29462
1 parent 9d1dfc7 commit 0348a7b

File tree

7 files changed

+372
-38
lines changed

7 files changed

+372
-38
lines changed

spring-web/src/main/java/org/springframework/http/ResponseEntity.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,13 @@ public static <T> ResponseEntity<T> of(Optional<T> body) {
263263
}
264264

265265
/**
266-
* Create a builder for a {@code ResponseEntity} with the given
267-
* {@link ProblemDetail} as the body, and its
268-
* {@link ProblemDetail#getStatus() status} as the status.
269-
* <p>Note that {@code ProblemDetail} is supported as a return value from
270-
* controller methods and from {@code @ExceptionHandler} methods. The method
271-
* here is convenient to also add response headers.
272-
* @param body the details for an HTTP error response
266+
* Create a new {@link HeadersBuilder} with its status set to
267+
* {@link ProblemDetail#getStatus()} and its body is set to
268+
* {@link ProblemDetail}.
269+
* <p><strong>Note:</strong> If there are no headers to add, there is usually
270+
* no need to create a {@link ResponseEntity} since {@code ProblemDetail}
271+
* is also supported as a return value from controller methods.
272+
* @param body the problem detail to use
273273
* @return the created builder
274274
* @since 6.0
275275
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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 java.net.URI;
20+
import java.util.function.Consumer;
21+
22+
import org.springframework.http.HttpHeaders;
23+
import org.springframework.http.HttpStatusCode;
24+
import org.springframework.http.ProblemDetail;
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.util.Assert;
27+
28+
29+
/**
30+
* Default implementation of {@link ErrorResponse.Builder}.
31+
*
32+
* @author Rossen Stoyanchev
33+
* @since 6.0
34+
*/
35+
final class DefaultErrorResponseBuilder implements ErrorResponse.Builder {
36+
37+
private final Throwable exception;
38+
39+
private final HttpStatusCode statusCode;
40+
41+
@Nullable
42+
private HttpHeaders headers;
43+
44+
private final ProblemDetail problemDetail;
45+
46+
private String detailMessageCode;
47+
48+
@Nullable
49+
private Object[] detailMessageArguments;
50+
51+
private String titleMessageCode;
52+
53+
54+
DefaultErrorResponseBuilder(Throwable ex, HttpStatusCode statusCode, String detail) {
55+
Assert.notNull(ex, "Throwable is required");
56+
Assert.notNull(ex, "HttpStatusCode is required");
57+
Assert.notNull(ex, "`detail` is required");
58+
this.exception = ex;
59+
this.statusCode = statusCode;
60+
this.problemDetail = ProblemDetail.forStatusAndDetail(statusCode, detail);
61+
this.detailMessageCode = ErrorResponse.getDefaultDetailMessageCode(ex.getClass(), null);
62+
this.titleMessageCode = ErrorResponse.getDefaultTitleMessageCode(ex.getClass());
63+
}
64+
65+
66+
@Override
67+
public ErrorResponse.Builder header(String headerName, String... headerValues) {
68+
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
69+
for (String headerValue : headerValues) {
70+
this.headers.add(headerName, headerValue);
71+
}
72+
return this;
73+
}
74+
75+
@Override
76+
public ErrorResponse.Builder headers(Consumer<HttpHeaders> headersConsumer) {
77+
return this;
78+
}
79+
80+
@Override
81+
public ErrorResponse.Builder detail(String detail) {
82+
this.problemDetail.setDetail(detail);
83+
return this;
84+
}
85+
86+
@Override
87+
public ErrorResponse.Builder detailMessageCode(String messageCode) {
88+
Assert.notNull(messageCode, "`detailMessageCode` is required");
89+
this.detailMessageCode = messageCode;
90+
return this;
91+
}
92+
93+
@Override
94+
public ErrorResponse.Builder detailMessageArguments(Object... messageArguments) {
95+
this.detailMessageArguments = messageArguments;
96+
return this;
97+
}
98+
99+
@Override
100+
public ErrorResponse.Builder type(URI type) {
101+
this.problemDetail.setType(type);
102+
return this;
103+
}
104+
105+
@Override
106+
public ErrorResponse.Builder title(@Nullable String title) {
107+
this.problemDetail.setTitle(title);
108+
return this;
109+
}
110+
111+
@Override
112+
public ErrorResponse.Builder titleMessageCode(String messageCode) {
113+
Assert.notNull(messageCode, "`titleMessageCode` is required");
114+
this.titleMessageCode = messageCode;
115+
return this;
116+
}
117+
118+
@Override
119+
public ErrorResponse.Builder instance(@Nullable URI instance) {
120+
this.problemDetail.setInstance(instance);
121+
return this;
122+
}
123+
124+
@Override
125+
public ErrorResponse.Builder property(String name, Object value) {
126+
this.problemDetail.setProperty(name, value);
127+
return this;
128+
}
129+
130+
@Override
131+
public ErrorResponse build() {
132+
return new SimpleErrorResponse(
133+
this.exception, this.statusCode, this.headers, this.problemDetail,
134+
this.detailMessageCode, this.detailMessageArguments, this.titleMessageCode);
135+
}
136+
137+
138+
/**
139+
* Simple container for {@code ErrorResponse} values.
140+
*/
141+
private static class SimpleErrorResponse implements ErrorResponse {
142+
143+
private final Throwable exception;
144+
145+
private final HttpStatusCode statusCode;
146+
147+
private final HttpHeaders headers;
148+
149+
private final ProblemDetail problemDetail;
150+
151+
private final String detailMessageCode;
152+
153+
@Nullable
154+
private final Object[] detailMessageArguments;
155+
156+
private final String titleMessageCode;
157+
158+
SimpleErrorResponse(
159+
Throwable ex, HttpStatusCode statusCode, @Nullable HttpHeaders headers, ProblemDetail problemDetail,
160+
String detailMessageCode, @Nullable Object[] detailMessageArguments, String titleMessageCode) {
161+
162+
this.exception = ex;
163+
this.statusCode = statusCode;
164+
this.headers = (headers != null ? headers : HttpHeaders.EMPTY);
165+
this.problemDetail = problemDetail;
166+
this.detailMessageCode = detailMessageCode;
167+
this.detailMessageArguments = detailMessageArguments;
168+
this.titleMessageCode = titleMessageCode;
169+
}
170+
171+
@Override
172+
public HttpStatusCode getStatusCode() {
173+
return this.statusCode;
174+
}
175+
176+
@Override
177+
public HttpHeaders getHeaders() {
178+
return this.headers;
179+
}
180+
181+
@Override
182+
public ProblemDetail getBody() {
183+
return this.problemDetail;
184+
}
185+
186+
@Override
187+
public String getDetailMessageCode() {
188+
return this.detailMessageCode;
189+
}
190+
191+
@Override
192+
public Object[] getDetailMessageArguments() {
193+
return this.detailMessageArguments;
194+
}
195+
196+
@Override
197+
public String getTitleMessageCode() {
198+
return this.titleMessageCode;
199+
}
200+
201+
@Override
202+
public String toString() {
203+
return "ErrorResponse{status=" + this.statusCode + ", " +
204+
"headers=" + this.headers + ", body=" + this.problemDetail + ", " +
205+
"exception=" + this.exception + "}";
206+
}
207+
}
208+
209+
}

spring-web/src/main/java/org/springframework/web/ErrorResponse.java

+114-23
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.web;
1818

19+
import java.net.URI;
1920
import java.util.Locale;
21+
import java.util.function.Consumer;
2022

2123
import org.springframework.context.MessageSource;
2224
import org.springframework.http.HttpHeaders;
@@ -148,35 +150,124 @@ static String getDefaultTitleMessageCode(Class<?> exceptionType) {
148150
return "problemDetail.title." + exceptionType.getName();
149151
}
150152

153+
151154
/**
152-
* Map the given Exception to an {@link ErrorResponse}.
153-
* @param ex the Exception, mostly to derive message codes, if not provided
154-
* @param status the response status to use
155-
* @param headers optional headers to add to the response
156-
* @param defaultDetail default value for the "detail" field
157-
* @param detailMessageCode the code to use to look up the "detail" field
158-
* through a {@code MessageSource}, falling back on
159-
* {@link #getDefaultDetailMessageCode(Class, String)}
160-
* @param detailMessageArguments the arguments to go with the detailMessageCode
161-
* @return the created {@code ErrorResponse} instance
155+
* Static factory method to build an instance via
156+
* {@link #builder(Throwable, HttpStatusCode, String)}.
162157
*/
163-
static ErrorResponse createFor(
164-
Exception ex, HttpStatusCode status, @Nullable HttpHeaders headers,
165-
String defaultDetail, @Nullable String detailMessageCode, @Nullable Object[] detailMessageArguments) {
158+
static ErrorResponse create(Throwable ex, HttpStatusCode statusCode, String detail) {
159+
return builder(ex, statusCode, detail).build();
160+
}
166161

167-
if (detailMessageCode == null) {
168-
detailMessageCode = ErrorResponse.getDefaultDetailMessageCode(ex.getClass(), null);
169-
}
162+
/**
163+
* Return a builder to create an {@code ErrorResponse} instance.
164+
* @param ex the underlying exception that lead to the error response;
165+
* mainly to derive default values for the
166+
* {@link #getDetailMessageCode() detail message code} and for the
167+
* {@link #getTitleMessageCode() title message code}.
168+
* @param statusCode the status code to set the response to
169+
* @param detail the default value for the
170+
* {@link ProblemDetail#setDetail(String) detail} field, unless overridden
171+
* by a {@link MessageSource} lookup with {@link #getDetailMessageCode()}
172+
*/
173+
static Builder builder(Throwable ex, HttpStatusCode statusCode, String detail) {
174+
return new DefaultErrorResponseBuilder(ex, statusCode, detail);
175+
}
170176

171-
ErrorResponseException errorResponse = new ErrorResponseException(
172-
status, ProblemDetail.forStatusAndDetail(status, defaultDetail), null,
173-
detailMessageCode, detailMessageArguments);
174177

175-
if (headers != null) {
176-
errorResponse.getHeaders().putAll(headers);
177-
}
178+
/**
179+
* Builder for an {@code ErrorResponse}.
180+
*/
181+
interface Builder {
182+
183+
/**
184+
* Add the given header value(s) under the given name.
185+
* @param headerName the header name
186+
* @param headerValues the header value(s)
187+
* @return the same builder instance
188+
* @see HttpHeaders#add(String, String)
189+
*/
190+
Builder header(String headerName, String... headerValues);
191+
192+
/**
193+
* Manipulate this response's headers with the given consumer. This is
194+
* useful to {@linkplain HttpHeaders#set(String, String) overwrite} or
195+
* {@linkplain HttpHeaders#remove(Object) remove} existing values, or
196+
* use any other {@link HttpHeaders} methods.
197+
* @param headersConsumer a function that consumes the {@code HttpHeaders}
198+
* @return the same builder instance
199+
*/
200+
Builder headers(Consumer<HttpHeaders> headersConsumer);
201+
202+
/**
203+
* Set the underlying {@link ProblemDetail#setDetail(String)}.
204+
* @return the same builder instance
205+
*/
206+
Builder detail(String detail);
207+
208+
/**
209+
* Customize the {@link MessageSource} code for looking up the value for
210+
* the underlying {@link #detail(String)}.
211+
* <p>By default, this is set to
212+
* {@link ErrorResponse#getDefaultDetailMessageCode(Class, String)} with the
213+
* associated Exception type.
214+
* @param messageCode the message code to use
215+
* @return the same builder instance
216+
* @see ErrorResponse#getDetailMessageCode()
217+
*/
218+
Builder detailMessageCode(String messageCode);
219+
220+
/**
221+
* Set the arguments to provide to the {@link MessageSource} lookup for
222+
* {@link #detailMessageCode(String)}.
223+
* @param messageArguments the arguments to provide
224+
* @return the same builder instance
225+
* @see ErrorResponse#getDetailMessageArguments()
226+
*/
227+
Builder detailMessageArguments(Object... messageArguments);
228+
229+
/**
230+
* Set the underlying {@link ProblemDetail#setTitle(String)} field.
231+
* @return the same builder instance
232+
*/
233+
Builder type(URI type);
234+
235+
/**
236+
* Set the underlying {@link ProblemDetail#setTitle(String)} field.
237+
* @return the same builder instance
238+
*/
239+
Builder title(@Nullable String title);
240+
241+
/**
242+
* Customize the {@link MessageSource} code for looking up the value for
243+
* the underlying {@link ProblemDetail#setTitle(String)}.
244+
* <p>By default, set via
245+
* {@link ErrorResponse#getDefaultTitleMessageCode(Class)} with the
246+
* associated Exception type.
247+
* @param messageCode the message code to use
248+
* @return the same builder instance
249+
* @see ErrorResponse#getTitleMessageCode()
250+
*/
251+
Builder titleMessageCode(String messageCode);
252+
253+
/**
254+
* Set the underlying {@link ProblemDetail#setInstance(URI)} field.
255+
* @return the same builder instance
256+
*/
257+
Builder instance(@Nullable URI instance);
258+
259+
/**
260+
* Set a "dynamic" {@link ProblemDetail#setProperty(String, Object)
261+
* property} on the underlying {@code ProblemDetail}.
262+
* @return the same builder instance
263+
*/
264+
Builder property(String name, Object value);
265+
266+
/**
267+
* Build the {@code ErrorResponse} instance.
268+
*/
269+
ErrorResponse build();
178270

179-
return errorResponse;
180271
}
181272

182273
}

0 commit comments

Comments
 (0)