Skip to content

Commit 5d0f49c

Browse files
committed
Add WebInputException subclasses
Closes gh-28142
1 parent 06e1cc2 commit 5d0f49c

15 files changed

+261
-79
lines changed

spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeBindException.java

+4-5
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.
@@ -31,11 +31,10 @@
3131
import org.springframework.validation.ObjectError;
3232
import org.springframework.web.server.ServerWebInputException;
3333

34+
3435
/**
35-
* A specialization of {@link ServerWebInputException} thrown when after data
36-
* binding and validation failure. Implements {@link BindingResult} (and its
37-
* super-interface {@link Errors}) to allow for direct analysis of binding and
38-
* validation errors.
36+
* {@link ServerWebInputException} subclass that indicates a data binding or
37+
* validation failure.
3938
*
4039
* @author Rossen Stoyanchev
4140
* @since 5.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.server;
18+
19+
20+
import org.springframework.core.MethodParameter;
21+
22+
23+
/**
24+
* {@link ServerWebInputException} subclass that indicates a missing request
25+
* value such as a request header, cookie value, query parameter, etc.
26+
*
27+
* @author Rossen Stoyanchev
28+
* @since 6.0
29+
*/
30+
@SuppressWarnings("serial")
31+
public class MissingRequestValueException extends ServerWebInputException {
32+
33+
private final String name;
34+
35+
private final Class<?> type;
36+
37+
private final String label;
38+
39+
40+
public MissingRequestValueException(String name, Class<?> type, String label, MethodParameter parameter) {
41+
super("Required " + label + " '" + name + "' is not present.", parameter);
42+
this.name = name;
43+
this.type = type;
44+
this.label = label;
45+
getBody().withDetail(getReason());
46+
}
47+
48+
49+
/**
50+
* Return the name of the missing value, e.g. the name of the missing request
51+
* header, or cookie, etc.
52+
*/
53+
public String getName() {
54+
return this.name;
55+
}
56+
57+
/**
58+
* Return the target type the value is converted when present.
59+
*/
60+
public Class<?> getType() {
61+
return this.type;
62+
}
63+
64+
/**
65+
* Return a label that describes the request value, e.g. "request header",
66+
* "cookie value", etc. Use this to create a custom message.
67+
*/
68+
public String getLabel() {
69+
return this.label;
70+
}
71+
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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.server;
18+
19+
20+
import java.util.List;
21+
22+
import org.springframework.util.MultiValueMap;
23+
24+
/**
25+
* {@link ServerWebInputException} subclass that indicates an unsatisfied
26+
* parameter condition, as typically expressed using an {@code @RequestMapping}
27+
* annotation at the {@code @Controller} type level.
28+
*
29+
* @author Rossen Stoyanchev
30+
* @since 6.0
31+
*/
32+
@SuppressWarnings("serial")
33+
public class UnsatisfiedRequestParameterException extends ServerWebInputException {
34+
35+
private final List<String> conditions;
36+
37+
private final MultiValueMap<String, String> requestParams;
38+
39+
40+
public UnsatisfiedRequestParameterException(
41+
List<String> conditions, MultiValueMap<String, String> requestParams) {
42+
43+
super(initReason(conditions, requestParams));
44+
this.conditions = conditions;
45+
this.requestParams = requestParams;
46+
getBody().withDetail("Invalid request parameters.");
47+
}
48+
49+
private static String initReason(List<String> conditions, MultiValueMap<String, String> queryParams) {
50+
StringBuilder sb = new StringBuilder("Parameter conditions ");
51+
int i = 0;
52+
for (String condition : conditions) {
53+
if (i > 0) {
54+
sb.append(" OR ");
55+
}
56+
sb.append('"').append(condition).append('"');
57+
i++;
58+
}
59+
sb.append(" not met for actual request parameters: ").append(queryParams);
60+
return sb.toString();
61+
}
62+
63+
64+
/**
65+
* Return String representations of the unsatisfied condition(s).
66+
*/
67+
public List<String> getConditions() {
68+
return this.conditions;
69+
}
70+
71+
/**
72+
* Return the actual request parameters.
73+
*/
74+
public MultiValueMap<String, String> getRequestParams() {
75+
return this.requestParams;
76+
}
77+
78+
}

spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java

+26
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.http.MediaType;
3030
import org.springframework.http.ProblemDetail;
3131
import org.springframework.lang.Nullable;
32+
import org.springframework.util.LinkedMultiValueMap;
3233
import org.springframework.validation.BindException;
3334
import org.springframework.validation.BindingResult;
3435
import org.springframework.validation.FieldError;
@@ -43,7 +44,9 @@
4344
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
4445
import org.springframework.web.multipart.support.MissingServletRequestPartException;
4546
import org.springframework.web.server.MethodNotAllowedException;
47+
import org.springframework.web.server.MissingRequestValueException;
4648
import org.springframework.web.server.NotAcceptableStatusException;
49+
import org.springframework.web.server.UnsatisfiedRequestParameterException;
4750
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
4851
import org.springframework.web.testfixture.method.ResolvableMethod;
4952

@@ -288,6 +291,29 @@ void notAcceptableStatusExceptionWithParseError() {
288291
assertThat(ex.getHeaders()).isEmpty();
289292
}
290293

294+
@Test
295+
void missingRequestValueException() {
296+
297+
ErrorResponse ex = new MissingRequestValueException(
298+
"foo", String.class, "header", this.methodParameter);
299+
300+
assertStatus(ex, HttpStatus.BAD_REQUEST);
301+
assertDetail(ex, "Required header 'foo' is not present.");
302+
assertThat(ex.getHeaders()).isEmpty();
303+
}
304+
305+
@Test
306+
void unsatisfiedRequestParameterException() {
307+
308+
ErrorResponse ex = new UnsatisfiedRequestParameterException(
309+
Arrays.asList("foo=bar", "bar=baz"),
310+
new LinkedMultiValueMap<>(Collections.singletonMap("q", Arrays.asList("1", "2"))));
311+
312+
assertStatus(ex, HttpStatus.BAD_REQUEST);
313+
assertDetail(ex, "Invalid request parameters.");
314+
assertThat(ex.getHeaders()).isEmpty();
315+
}
316+
291317
@Test
292318
void webExchangeBindException() {
293319

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java

+30-30
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.
@@ -46,6 +46,7 @@
4646
import org.springframework.web.server.NotAcceptableStatusException;
4747
import org.springframework.web.server.ServerWebExchange;
4848
import org.springframework.web.server.ServerWebInputException;
49+
import org.springframework.web.server.UnsatisfiedRequestParameterException;
4950
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
5051
import org.springframework.web.util.pattern.PathPattern;
5152

@@ -190,7 +191,8 @@ protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> infos,
190191
catch (InvalidMediaTypeException ex) {
191192
throw new UnsupportedMediaTypeStatusException(ex.getMessage());
192193
}
193-
throw new UnsupportedMediaTypeStatusException(contentType, new ArrayList<>(mediaTypes), exchange.getRequest().getMethod());
194+
throw new UnsupportedMediaTypeStatusException(
195+
contentType, new ArrayList<>(mediaTypes), exchange.getRequest().getMethod());
194196
}
195197

196198
if (helper.hasProducesMismatch()) {
@@ -199,9 +201,9 @@ protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> infos,
199201
}
200202

201203
if (helper.hasParamsMismatch()) {
202-
throw new ServerWebInputException(
203-
"Expected parameters: " + helper.getParamConditions() +
204-
", actual query parameters: " + request.getQueryParams());
204+
throw new UnsatisfiedRequestParameterException(
205+
helper.getParamConditions().stream().map(Object::toString).toList(),
206+
request.getQueryParams());
205207
}
206208

207209
return null;
@@ -217,10 +219,9 @@ private static class PartialMatchHelper {
217219

218220

219221
public PartialMatchHelper(Set<RequestMappingInfo> infos, ServerWebExchange exchange) {
220-
this.partialMatches.addAll(infos.stream().
221-
filter(info -> info.getPatternsCondition().getMatchingCondition(exchange) != null).
222-
map(info -> new PartialMatch(info, exchange)).
223-
collect(Collectors.toList()));
222+
this.partialMatches.addAll(infos.stream()
223+
.filter(info -> info.getPatternsCondition().getMatchingCondition(exchange) != null)
224+
.map(info -> new PartialMatch(info, exchange)).toList());
224225
}
225226

226227

@@ -235,72 +236,71 @@ public boolean isEmpty() {
235236
* Any partial matches for "methods"?
236237
*/
237238
public boolean hasMethodsMismatch() {
238-
return this.partialMatches.stream().
239-
noneMatch(PartialMatch::hasMethodsMatch);
239+
return this.partialMatches.stream().noneMatch(PartialMatch::hasMethodsMatch);
240240
}
241241

242242
/**
243243
* Any partial matches for "methods" and "consumes"?
244244
*/
245245
public boolean hasConsumesMismatch() {
246-
return this.partialMatches.stream().
247-
noneMatch(PartialMatch::hasConsumesMatch);
246+
return this.partialMatches.stream().noneMatch(PartialMatch::hasConsumesMatch);
248247
}
249248

250249
/**
251250
* Any partial matches for "methods", "consumes", and "produces"?
252251
*/
253252
public boolean hasProducesMismatch() {
254-
return this.partialMatches.stream().
255-
noneMatch(PartialMatch::hasProducesMatch);
253+
return this.partialMatches.stream().noneMatch(PartialMatch::hasProducesMatch);
256254
}
257255

258256
/**
259257
* Any partial matches for "methods", "consumes", "produces", and "params"?
260258
*/
261259
public boolean hasParamsMismatch() {
262-
return this.partialMatches.stream().
263-
noneMatch(PartialMatch::hasParamsMatch);
260+
return this.partialMatches.stream().noneMatch(PartialMatch::hasParamsMatch);
264261
}
265262

266263
/**
267264
* Return declared HTTP methods.
268265
*/
269266
public Set<HttpMethod> getAllowedMethods() {
270-
return this.partialMatches.stream().
271-
flatMap(m -> m.getInfo().getMethodsCondition().getMethods().stream()).
272-
map(requestMethod -> HttpMethod.valueOf(requestMethod.name())).
273-
collect(Collectors.toSet());
267+
return this.partialMatches.stream()
268+
.flatMap(m -> m.getInfo().getMethodsCondition().getMethods().stream())
269+
.map(requestMethod -> HttpMethod.valueOf(requestMethod.name()))
270+
.collect(Collectors.toSet());
274271
}
275272

276273
/**
277274
* Return declared "consumable" types but only among those that also
278275
* match the "methods" condition.
279276
*/
280277
public Set<MediaType> getConsumableMediaTypes() {
281-
return this.partialMatches.stream().filter(PartialMatch::hasMethodsMatch).
282-
flatMap(m -> m.getInfo().getConsumesCondition().getConsumableMediaTypes().stream()).
283-
collect(Collectors.toCollection(LinkedHashSet::new));
278+
return this.partialMatches.stream()
279+
.filter(PartialMatch::hasMethodsMatch)
280+
.flatMap(m -> m.getInfo().getConsumesCondition().getConsumableMediaTypes().stream())
281+
.collect(Collectors.toCollection(LinkedHashSet::new));
284282
}
285283

286284
/**
287285
* Return declared "producible" types but only among those that also
288286
* match the "methods" and "consumes" conditions.
289287
*/
290288
public Set<MediaType> getProducibleMediaTypes() {
291-
return this.partialMatches.stream().filter(PartialMatch::hasConsumesMatch).
292-
flatMap(m -> m.getInfo().getProducesCondition().getProducibleMediaTypes().stream()).
293-
collect(Collectors.toCollection(LinkedHashSet::new));
289+
return this.partialMatches.stream()
290+
.filter(PartialMatch::hasConsumesMatch)
291+
.flatMap(m -> m.getInfo().getProducesCondition().getProducibleMediaTypes().stream())
292+
.collect(Collectors.toCollection(LinkedHashSet::new));
294293
}
295294

296295
/**
297296
* Return declared "params" conditions but only among those that also
298297
* match the "methods", "consumes", and "params" conditions.
299298
*/
300299
public List<Set<NameValueExpression<String>>> getParamConditions() {
301-
return this.partialMatches.stream().filter(PartialMatch::hasProducesMatch).
302-
map(match -> match.getInfo().getParamsCondition().getExpressions()).
303-
collect(Collectors.toList());
300+
return this.partialMatches.stream()
301+
.filter(PartialMatch::hasProducesMatch)
302+
.map(match -> match.getInfo().getParamsCondition().getExpressions())
303+
.collect(Collectors.toList());
304304
}
305305

306306
/**

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java

+9-3
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.
@@ -225,8 +225,14 @@ private Throwable handleReadError(MethodParameter parameter, Throwable ex) {
225225
}
226226

227227
private ServerWebInputException handleMissingBody(MethodParameter parameter) {
228-
String paramInfo = parameter.getExecutable().toGenericString();
229-
return new ServerWebInputException("Request body is missing: " + paramInfo, parameter);
228+
229+
DecodingException cause = new DecodingException(
230+
"No request body for: " + parameter.getExecutable().toGenericString());
231+
232+
ServerWebInputException ex = new ServerWebInputException("No request body", parameter, cause);
233+
ex.setDetail("Invalid request content");
234+
235+
return ex;
230236
}
231237

232238
/**

0 commit comments

Comments
 (0)