user4768018
user4768018

Reputation: 131

Spring restController: how to error when unknown @RequestParam is in url

I'm using spring 4.2 to create some restfull webservices. But we realized that when a user mistypes one of the not-mandatory @RequestParam, we do not get an error that the param he passed is unknown.

like we have @RequestParam(required=false, value="valueA") String value A and in the call he uses '?valuueA=AA' -> we want an error. But I do not seem to find a way to do this, the value is just ignored and the user is unaware of this.

Upvotes: 8

Views: 3054

Answers (2)

Alexander Taylor
Alexander Taylor

Reputation: 17642

I translated Bohuslav Burghardt's solution for Spring WebFlux applications.

I dropped the @DisallowUndeclaredRequestParams annotation class from GitHub because I didn't need it -- it just applies the filter to all HandlerMethods. But someone else could update this answer and put it back.

package com.example.springundeclaredparamerror;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

/**
 * Handler interceptor used for ensuring that no request params other than those explicitly
 * declared via {@link RequestParam} parameters of the handler method are passed in.
 */
// Implementation translated into WebFlux WebFilter from:
// https://github.com/bohuslav-burghardt/spring-sandbox/tree/master/handler-interceptors/src/main/java/handler_interceptors
@Component
public class DisallowUndeclaredParamsFilter implements WebFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(DisallowUndeclaredParamsFilter.class);

    @Autowired
    @Qualifier("requestMappingHandlerMapping")
    RequestMappingHandlerMapping mapping;

    @Autowired
    ObjectMapper mapper;

    @Override
    public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
        Object o = mapping.getHandler(serverWebExchange).toFuture().getNow(null);
        Optional<String> undeclaredParam = Optional.empty();
        if (o != null && o instanceof HandlerMethod) {
            var handlerMethod = (HandlerMethod) o;
            undeclaredParam = checkParams(serverWebExchange.getRequest(),
                    getDeclaredRequestParams(handlerMethod));
        }

        return undeclaredParam.map((param) -> RespondWithError(serverWebExchange, param))
                .orElseGet(() -> webFilterChain.filter(serverWebExchange));
    }

    /** Responds to the request with an error message for the given undeclared parameter. */
    private Mono<Void> RespondWithError(ServerWebExchange serverWebExchange, String undeclaredParam) {
        final HttpStatus status = HttpStatus.BAD_REQUEST;
        serverWebExchange.getResponse().setStatusCode(status);
        serverWebExchange.getResponse().getHeaders().add(
                "Content-Type", "application/json");
        UndeclaredParamErrorResponse response = new UndeclaredParamErrorResponse();
        response.message = "Parameter not expected: " + undeclaredParam;
        response.statusCode = status.value();
        String error = null;
        try {
            error = mapper.writeValueAsString(response);
        } catch (JsonProcessingException e) {
            error = "Parameter not expected; error generating JSON response";
            LOGGER.warn("Error generating JSON response for undeclared argument", e);
        }
        return serverWebExchange.getResponse().writeAndFlushWith(
                Mono.just(Mono.just(serverWebExchange.getResponse().bufferFactory().wrap(
                        error.getBytes(StandardCharsets.UTF_8)))));
    }

    /** Structure for generating error JSON. */
    static class UndeclaredParamErrorResponse {
        public String message;
        public int statusCode;
    }

    /**
     * Check that all of the request params of the specified request are contained within the specified set of allowed
     * parameters.
     *
     * @param request       Request whose params to check.
     * @param allowedParams Set of allowed request parameters.
     * @return Name of a param in the request that is not allowed, or empty if all params in the request are allowed.
     */
    private Optional<String> checkParams(ServerHttpRequest request, Set<String> allowedParams) {
        return request.getQueryParams().keySet().stream().filter(param ->
                !allowedParams.contains(param)
        ).findFirst();
    }

    /**
     * Extract all request parameters declared via {@link RequestParam} for the specified handler method.
     *
     * @param handlerMethod Handler method to extract declared params for.
     * @return Set of declared request parameters.
     */
    private Set<String> getDeclaredRequestParams(HandlerMethod handlerMethod) {
        Set<String> declaredRequestParams = new HashSet<>();
        MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
        ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

        for (MethodParameter methodParameter : methodParameters) {
            if (methodParameter.hasParameterAnnotation(RequestParam.class)) {
                RequestParam requestParam = methodParameter.getParameterAnnotation(RequestParam.class);
                if (StringUtils.hasText(requestParam.value())) {
                    declaredRequestParams.add(requestParam.value());
                } else {
                    methodParameter.initParameterNameDiscovery(parameterNameDiscoverer);
                    declaredRequestParams.add(methodParameter.getParameterName());
                }
            }
        }
        return declaredRequestParams;
    }
}

Here's the unit test I wrote for it. I recommend checking it into your codebase as well.

package com.example.springundeclaredparamerror;

import com.github.tomakehurst.wiremock.junit.WireMockRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;

@RunWith(SpringRunner.class)
@WebFluxTest(controllers = {DisallowUndeclaredParamFilterTest.TestController.class})
public class DisallowUndeclaredParamFilterTest {
    private static final String TEST_ENDPOINT = "/disallowUndeclaredParamFilterTest";

    @Rule
    public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());

    @Autowired
    private WebTestClient webClient;

    @Configuration
    @Import({TestController.class, DisallowUndeclaredParamsFilter.class})
    static class TestConfig {
    }

    @RestController
    static class TestController {
        @GetMapping(TEST_ENDPOINT)
        public Mono<String> retrieveEntity(@RequestParam(name = "a", required = false) final String a) {
            return Mono.just("ok");
        }
    }

    @Test
    public void testAllowsNoArgs() {
        webClient.get().uri(TEST_ENDPOINT).exchange().expectBody(String.class).isEqualTo("ok");
    }

    @Test
    public void testAllowsDeclaredArg() {
        webClient.get().uri(TEST_ENDPOINT + "?a=1").exchange().expectBody(String.class).isEqualTo("ok");
    }

    @Test
    public void testDisallowsUndeclaredArg() {
        webClient.get().uri(TEST_ENDPOINT + "?b=1").exchange().expectStatus().is4xxClientError();
    }
}

Upvotes: 0

Bohuslav Burghardt
Bohuslav Burghardt

Reputation: 34776

One possible solution would be to create an implementation of HandlerInterceptor which will verify that all request parameters passed to the handler method are declared in its @RequestParam annotated parameters.

However you should consider the disadvantages of such solution. There might be situations where you want to allow certain parameters to be passed in and not be declared as request params. For instance if you have request like GET /foo?page=1&offset=0 and have handler with following signature:

@RequestMapping
public List<Foo> listFoos(PagingParams page);

and PagingParams is a class containing page and offset properties, it will normally be mapped from the request parameters. Implementation of a solution you want would interfere with this Spring MVC'c functionality.


That being said, here is a sample implementation I had in mind:

public class UndeclaredParamsHandlerInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            checkParams(request, getDeclaredRequestParams(handlerMethod));
        }
        return true;
    }

    private void checkParams(HttpServletRequest request, Set<String> allowedParams) {
        request.getParameterMap().entrySet().forEach(entry -> {
            String param = entry.getKey();
            if (!allowedParams.contains(param)) {
                throw new UndeclaredRequestParamException(param, allowedParams);
            }
        });
    }

    private Set<String> getDeclaredRequestParams(HandlerMethod handlerMethod) {
        Set<String> declaredRequestParams = new HashSet<>();
        MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
        ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

        for (MethodParameter methodParameter : methodParameters) {
            if (methodParameter.hasParameterAnnotation(RequestParam.class)) {
                RequestParam requestParam = methodParameter.getParameterAnnotation(RequestParam.class);
                if (StringUtils.hasText(requestParam.value())) {
                    declaredRequestParams.add(requestParam.value());
                } else {
                    methodParameter.initParameterNameDiscovery(parameterNameDiscoverer);
                    declaredRequestParams.add(methodParameter.getParameterName());
                }
            }
        }
        return declaredRequestParams;
    }

}

Basically this will do what I described above. You can then add exception handler for the exception it throws and translate it to HTTP 400 response. I've put more of an complete sample on Github, which includes a way to selectively enable this behavior for individual handler methods via annotation.

Upvotes: 4

Related Questions