Skip to content

Commit

Permalink
Add WebInputException subclasses
Browse files Browse the repository at this point in the history
Closes gh-28142
  • Loading branch information
rstoyanchev committed May 9, 2022
1 parent 06e1cc2 commit 5d0f49c
Show file tree
Hide file tree
Showing 15 changed files with 261 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -31,11 +31,10 @@
import org.springframework.validation.ObjectError;
import org.springframework.web.server.ServerWebInputException;


/**
* A specialization of {@link ServerWebInputException} thrown when after data
* binding and validation failure. Implements {@link BindingResult} (and its
* super-interface {@link Errors}) to allow for direct analysis of binding and
* validation errors.
* {@link ServerWebInputException} subclass that indicates a data binding or
* validation failure.
*
* @author Rossen Stoyanchev
* @since 5.0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.server;


import org.springframework.core.MethodParameter;


/**
* {@link ServerWebInputException} subclass that indicates a missing request
* value such as a request header, cookie value, query parameter, etc.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
@SuppressWarnings("serial")
public class MissingRequestValueException extends ServerWebInputException {

private final String name;

private final Class<?> type;

private final String label;


public MissingRequestValueException(String name, Class<?> type, String label, MethodParameter parameter) {
super("Required " + label + " '" + name + "' is not present.", parameter);
this.name = name;
this.type = type;
this.label = label;
getBody().withDetail(getReason());
}


/**
* Return the name of the missing value, e.g. the name of the missing request
* header, or cookie, etc.
*/
public String getName() {
return this.name;
}

/**
* Return the target type the value is converted when present.
*/
public Class<?> getType() {
return this.type;
}

/**
* Return a label that describes the request value, e.g. "request header",
* "cookie value", etc. Use this to create a custom message.
*/
public String getLabel() {
return this.label;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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.server;


import java.util.List;

import org.springframework.util.MultiValueMap;

/**
* {@link ServerWebInputException} subclass that indicates an unsatisfied
* parameter condition, as typically expressed using an {@code @RequestMapping}
* annotation at the {@code @Controller} type level.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
@SuppressWarnings("serial")
public class UnsatisfiedRequestParameterException extends ServerWebInputException {

private final List<String> conditions;

private final MultiValueMap<String, String> requestParams;


public UnsatisfiedRequestParameterException(
List<String> conditions, MultiValueMap<String, String> requestParams) {

super(initReason(conditions, requestParams));
this.conditions = conditions;
this.requestParams = requestParams;
getBody().withDetail("Invalid request parameters.");
}

private static String initReason(List<String> conditions, MultiValueMap<String, String> queryParams) {
StringBuilder sb = new StringBuilder("Parameter conditions ");
int i = 0;
for (String condition : conditions) {
if (i > 0) {
sb.append(" OR ");
}
sb.append('"').append(condition).append('"');
i++;
}
sb.append(" not met for actual request parameters: ").append(queryParams);
return sb.toString();
}


/**
* Return String representations of the unsatisfied condition(s).
*/
public List<String> getConditions() {
return this.conditions;
}

/**
* Return the actual request parameters.
*/
public MultiValueMap<String, String> getRequestParams() {
return this.requestParams;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.lang.Nullable;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
Expand All @@ -43,7 +44,9 @@
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.MissingRequestValueException;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.UnsatisfiedRequestParameterException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import org.springframework.web.testfixture.method.ResolvableMethod;

Expand Down Expand Up @@ -288,6 +291,29 @@ void notAcceptableStatusExceptionWithParseError() {
assertThat(ex.getHeaders()).isEmpty();
}

@Test
void missingRequestValueException() {

ErrorResponse ex = new MissingRequestValueException(
"foo", String.class, "header", this.methodParameter);

assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Required header 'foo' is not present.");
assertThat(ex.getHeaders()).isEmpty();
}

@Test
void unsatisfiedRequestParameterException() {

ErrorResponse ex = new UnsatisfiedRequestParameterException(
Arrays.asList("foo=bar", "bar=baz"),
new LinkedMultiValueMap<>(Collections.singletonMap("q", Arrays.asList("1", "2"))));

assertStatus(ex, HttpStatus.BAD_REQUEST);
assertDetail(ex, "Invalid request parameters.");
assertThat(ex.getHeaders()).isEmpty();
}

@Test
void webExchangeBindException() {

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -46,6 +46,7 @@
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsatisfiedRequestParameterException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import org.springframework.web.util.pattern.PathPattern;

Expand Down Expand Up @@ -190,7 +191,8 @@ protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> infos,
catch (InvalidMediaTypeException ex) {
throw new UnsupportedMediaTypeStatusException(ex.getMessage());
}
throw new UnsupportedMediaTypeStatusException(contentType, new ArrayList<>(mediaTypes), exchange.getRequest().getMethod());
throw new UnsupportedMediaTypeStatusException(
contentType, new ArrayList<>(mediaTypes), exchange.getRequest().getMethod());
}

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

if (helper.hasParamsMismatch()) {
throw new ServerWebInputException(
"Expected parameters: " + helper.getParamConditions() +
", actual query parameters: " + request.getQueryParams());
throw new UnsatisfiedRequestParameterException(
helper.getParamConditions().stream().map(Object::toString).toList(),
request.getQueryParams());
}

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


public PartialMatchHelper(Set<RequestMappingInfo> infos, ServerWebExchange exchange) {
this.partialMatches.addAll(infos.stream().
filter(info -> info.getPatternsCondition().getMatchingCondition(exchange) != null).
map(info -> new PartialMatch(info, exchange)).
collect(Collectors.toList()));
this.partialMatches.addAll(infos.stream()
.filter(info -> info.getPatternsCondition().getMatchingCondition(exchange) != null)
.map(info -> new PartialMatch(info, exchange)).toList());
}


Expand All @@ -235,72 +236,71 @@ public boolean isEmpty() {
* Any partial matches for "methods"?
*/
public boolean hasMethodsMismatch() {
return this.partialMatches.stream().
noneMatch(PartialMatch::hasMethodsMatch);
return this.partialMatches.stream().noneMatch(PartialMatch::hasMethodsMatch);
}

/**
* Any partial matches for "methods" and "consumes"?
*/
public boolean hasConsumesMismatch() {
return this.partialMatches.stream().
noneMatch(PartialMatch::hasConsumesMatch);
return this.partialMatches.stream().noneMatch(PartialMatch::hasConsumesMatch);
}

/**
* Any partial matches for "methods", "consumes", and "produces"?
*/
public boolean hasProducesMismatch() {
return this.partialMatches.stream().
noneMatch(PartialMatch::hasProducesMatch);
return this.partialMatches.stream().noneMatch(PartialMatch::hasProducesMatch);
}

/**
* Any partial matches for "methods", "consumes", "produces", and "params"?
*/
public boolean hasParamsMismatch() {
return this.partialMatches.stream().
noneMatch(PartialMatch::hasParamsMatch);
return this.partialMatches.stream().noneMatch(PartialMatch::hasParamsMatch);
}

/**
* Return declared HTTP methods.
*/
public Set<HttpMethod> getAllowedMethods() {
return this.partialMatches.stream().
flatMap(m -> m.getInfo().getMethodsCondition().getMethods().stream()).
map(requestMethod -> HttpMethod.valueOf(requestMethod.name())).
collect(Collectors.toSet());
return this.partialMatches.stream()
.flatMap(m -> m.getInfo().getMethodsCondition().getMethods().stream())
.map(requestMethod -> HttpMethod.valueOf(requestMethod.name()))
.collect(Collectors.toSet());
}

/**
* Return declared "consumable" types but only among those that also
* match the "methods" condition.
*/
public Set<MediaType> getConsumableMediaTypes() {
return this.partialMatches.stream().filter(PartialMatch::hasMethodsMatch).
flatMap(m -> m.getInfo().getConsumesCondition().getConsumableMediaTypes().stream()).
collect(Collectors.toCollection(LinkedHashSet::new));
return this.partialMatches.stream()
.filter(PartialMatch::hasMethodsMatch)
.flatMap(m -> m.getInfo().getConsumesCondition().getConsumableMediaTypes().stream())
.collect(Collectors.toCollection(LinkedHashSet::new));
}

/**
* Return declared "producible" types but only among those that also
* match the "methods" and "consumes" conditions.
*/
public Set<MediaType> getProducibleMediaTypes() {
return this.partialMatches.stream().filter(PartialMatch::hasConsumesMatch).
flatMap(m -> m.getInfo().getProducesCondition().getProducibleMediaTypes().stream()).
collect(Collectors.toCollection(LinkedHashSet::new));
return this.partialMatches.stream()
.filter(PartialMatch::hasConsumesMatch)
.flatMap(m -> m.getInfo().getProducesCondition().getProducibleMediaTypes().stream())
.collect(Collectors.toCollection(LinkedHashSet::new));
}

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

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -225,8 +225,14 @@ private Throwable handleReadError(MethodParameter parameter, Throwable ex) {
}

private ServerWebInputException handleMissingBody(MethodParameter parameter) {
String paramInfo = parameter.getExecutable().toGenericString();
return new ServerWebInputException("Request body is missing: " + paramInfo, parameter);

DecodingException cause = new DecodingException(
"No request body for: " + parameter.getExecutable().toGenericString());

ServerWebInputException ex = new ServerWebInputException("No request body", parameter, cause);
ex.setDetail("Invalid request content");

return ex;
}

/**
Expand Down
Loading

0 comments on commit 5d0f49c

Please sign in to comment.