/
ErrorResponse.java
286 lines (251 loc) · 9.67 KB
/
ErrorResponse.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
/*
* 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 java.util.Locale;
import java.util.function.Consumer;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.lang.Nullable;
/**
* Representation of a complete RFC 7807 error response including status,
* headers, and an RFC 7807 formatted {@link ProblemDetail} body. Allows any
* exception to expose HTTP error response information.
*
* <p>{@link ErrorResponseException} is a default implementation of this
* interface and a convenient base class for other exceptions to use.
*
* <p>{@code ErrorResponse} is supported as a return value from
* {@code @ExceptionHandler} methods that render directly to the response, e.g.
* by being marked {@code @ResponseBody}, or declared in an
* {@code @RestController} or {@code RestControllerAdvice} class.
*
* @author Rossen Stoyanchev
* @since 6.0
* @see ErrorResponseException
*/
public interface ErrorResponse {
/**
* Return the HTTP status code to use for the response.
*/
HttpStatusCode getStatusCode();
/**
* 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();
/**
* Return a code to use to resolve the problem "detail" for this exception
* through a {@link MessageSource}.
* <p>By default this is initialized via
* {@link #getDefaultDetailMessageCode(Class, String)}.
*/
default String getDetailMessageCode() {
return getDefaultDetailMessageCode(getClass(), null);
}
/**
* Return arguments to use along with a {@link #getDetailMessageCode()
* message code} to resolve the problem "detail" for this exception
* through a {@link MessageSource}. The arguments are expanded
* into placeholders of the message value, e.g. "Invalid content type {0}".
*/
@Nullable
default Object[] getDetailMessageArguments() {
return null;
}
/**
* Variant of {@link #getDetailMessageArguments()} that uses the given
* {@link MessageSource} for resolving the message argument values.
* This is useful for example to message codes from validation errors.
*/
@Nullable
default Object[] getDetailMessageArguments(MessageSource messageSource, Locale locale) {
return getDetailMessageArguments();
}
/**
* Return a code to use to resolve the problem "detail" for this exception
* through a {@link MessageSource}.
* <p>By default this is initialized via
* {@link #getDefaultDetailMessageCode(Class, String)}.
*/
default String getTitleMessageCode() {
return getDefaultTitleMessageCode(getClass());
}
/**
* Resolve the {@link #getDetailMessageCode() detailMessageCode} and the
* {@link #getTitleMessageCode() titleCode} through the given
* {@link MessageSource}, and if found, update the "detail" and "title!"
* fields respectively.
* @param messageSource the {@code MessageSource} to use for the lookup
* @param locale the {@code Locale} to use for the lookup
*/
default ProblemDetail updateAndGetBody(@Nullable MessageSource messageSource, Locale locale) {
if (messageSource != null) {
Object[] arguments = getDetailMessageArguments(messageSource, locale);
String detail = messageSource.getMessage(getDetailMessageCode(), arguments, null, locale);
if (detail != null) {
getBody().setDetail(detail);
}
String title = messageSource.getMessage(getTitleMessageCode(), null, null, locale);
if (title != null) {
getBody().setTitle(title);
}
}
return getBody();
}
/**
* Build a message code for the "detail" field, for the given exception type.
* @param exceptionType the exception type associated with the problem
* @param suffix an optional suffix, e.g. for exceptions that may have multiple
* error message with different arguments.
* @return {@code "problemDetail."} followed by the full {@link Class#getName() class name}
*/
static String getDefaultDetailMessageCode(Class<?> exceptionType, @Nullable String suffix) {
return "problemDetail." + exceptionType.getName() + (suffix != null ? "." + suffix : "");
}
/**
* Build a message code for the "title" field, for the given exception type.
* @param exceptionType the exception type associated with the problem
* @return {@code "problemDetail.title."} followed by the full {@link Class#getName() class name}
*/
static String getDefaultTitleMessageCode(Class<?> exceptionType) {
return "problemDetail.title." + exceptionType.getName();
}
/**
* Static factory method to build an instance via
* {@link #builder(Throwable, HttpStatusCode, String)}.
*/
static ErrorResponse create(Throwable ex, HttpStatusCode statusCode, String detail) {
return builder(ex, statusCode, detail).build();
}
/**
* Return a builder to create an {@code ErrorResponse} instance.
* @param ex the underlying exception that lead to the error response;
* mainly to derive default values for the
* {@link #getDetailMessageCode() detail message code} and for the
* {@link #getTitleMessageCode() title message code}.
* @param statusCode the status code to set the response to
* @param detail the default value for the
* {@link ProblemDetail#setDetail(String) detail} field, unless overridden
* by a {@link MessageSource} lookup with {@link #getDetailMessageCode()}
*/
static Builder builder(Throwable ex, HttpStatusCode statusCode, String detail) {
return new DefaultErrorResponseBuilder(ex, statusCode, detail);
}
/**
* Builder for an {@code ErrorResponse}.
*/
interface Builder {
/**
* Add the given header value(s) under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @return the same builder instance
* @see HttpHeaders#add(String, String)
*/
Builder header(String headerName, String... headerValues);
/**
* Manipulate this response's headers with the given consumer. This is
* useful to {@linkplain HttpHeaders#set(String, String) overwrite} or
* {@linkplain HttpHeaders#remove(Object) remove} existing values, or
* use any other {@link HttpHeaders} methods.
* @param headersConsumer a function that consumes the {@code HttpHeaders}
* @return the same builder instance
*/
Builder headers(Consumer<HttpHeaders> headersConsumer);
/**
* Set the underlying {@link ProblemDetail#setDetail(String)}.
* @return the same builder instance
*/
Builder detail(String detail);
/**
* Customize the {@link MessageSource} code for looking up the value for
* the underlying {@link #detail(String)}.
* <p>By default, this is set to
* {@link ErrorResponse#getDefaultDetailMessageCode(Class, String)} with the
* associated Exception type.
* @param messageCode the message code to use
* @return the same builder instance
* @see ErrorResponse#getDetailMessageCode()
*/
Builder detailMessageCode(String messageCode);
/**
* Set the arguments to provide to the {@link MessageSource} lookup for
* {@link #detailMessageCode(String)}.
* @param messageArguments the arguments to provide
* @return the same builder instance
* @see ErrorResponse#getDetailMessageArguments()
*/
Builder detailMessageArguments(Object... messageArguments);
/**
* Set the underlying {@link ProblemDetail#setType(URI)} field.
* @return the same builder instance
*/
Builder type(URI type);
/**
* Set the underlying {@link ProblemDetail#setTitle(String)} field.
* @return the same builder instance
*/
Builder title(@Nullable String title);
/**
* Customize the {@link MessageSource} code for looking up the value for
* the underlying {@link ProblemDetail#setTitle(String)}.
* <p>By default, set via
* {@link ErrorResponse#getDefaultTitleMessageCode(Class)} with the
* associated Exception type.
* @param messageCode the message code to use
* @return the same builder instance
* @see ErrorResponse#getTitleMessageCode()
*/
Builder titleMessageCode(String messageCode);
/**
* Set the underlying {@link ProblemDetail#setInstance(URI)} field.
* @return the same builder instance
*/
Builder instance(@Nullable URI instance);
/**
* Set a "dynamic" {@link ProblemDetail#setProperty(String, Object)
* property} on the underlying {@code ProblemDetail}.
* @return the same builder instance
*/
Builder property(String name, Object value);
/**
* Build the {@code ErrorResponse} instance.
*/
ErrorResponse build();
/**
* Build the {@code ErrorResponse} instance and also resolve the "detail"
* and "title" through the given {@link MessageSource}. Effectively a
* shortcut for calling {@link #build()} and then
* {@link ErrorResponse#updateAndGetBody(MessageSource, Locale)}.
* @since 6.0.3
*/
default ErrorResponse build(@Nullable MessageSource messageSource, Locale locale) {
ErrorResponse response = build();
response.updateAndGetBody(messageSource, locale);
return response;
}
}
}