6

I have a basic rest controller taking parameters.

How can I refuse the connection if the query string contains parameters that I did not define?

@RestController
@RequestMapping("/")
public class MyRest {
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
    @ResponseBody
    public String content(@PathVariable id, @RequestParam(value = "page", required = false) int page) {
        return id;
    }
}

localhost:8080/myapp/123?pagggge=1

Currently when calling this url, the method is executed with just the id, and the unknown paggge parameter is just neglected. Which is fine in general, but how can I validate them and in case return a HTTP status code?

membersound
  • 81,582
  • 193
  • 585
  • 1,120
  • I'd like to know...why does it matter? If you're not accepting the parameter it does no harm to let it fall on the floor. – Makoto Feb 21 '15 at 08:09

4 Answers4

13

You may get all parameters incoming and handle in the way you want. Quoting spring documentation:

When an @RequestParam annotation is used on a Map<String, String> or MultiValueMap<String, String> argument, the map is populated with all request parameters.

ametiste
  • 386
  • 4
  • 14
5

In your controller method, you can include an argument of type @RequestParam Map<String, String> to get access to all query parameters passed in. A generic ArgsChecker service class can be used to check whether the user passed in an invalid argument. If so, you can throw an exception, which can be handled by an @ControllerAdvice class.

@RestController
@ExposesResourceFor(Widget.class)
@RequestMapping("/widgets")
public class WidgetController {


@Autowired
ArgsChecker<Widget> widgetArgsChecker;

    @RequestMapping(value = "", method = RequestMethod.GET, produces = {"application/hal+json"})
    public HttpEntity<PagedResources<WidgetResource>> findAll(@RequestParam @ApiIgnore Map<String, String> allRequestParams, Pageable pageable, PagedResourcesAssembler pageAssembler) {
        Set<String> invalidArgs = widgetArgsChecker.getInvalidArgs(allRequestParams.keySet());
        if (invalidArgs.size() > 0) {
            throw new QueryParameterNotSupportedException("The user supplied query parameter(s) that are not supported: " + invalidArgs + " . See below for a list of query paramters that are supported by the widget endpoint.", invalidArgs, widgetArgsChecker.getValidArgs());

        }

The ArgsChecker can be defined as follows:

import com.widgetstore.api.annotation.Queryable;
import lombok.Getter;
import org.apache.commons.lang3.reflect.FieldUtils;

import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

public class ArgsChecker<T> {
    @Getter
    private Set<String> validArgs;

    private ArgsChecker(){};

        public ArgsChecker(Class<T> someEntityClass){
    validArgs= FieldUtils.getFieldsListWithAnnotation(someEntityClass,Queryable.class)
            .stream()
            .map(Field::getName)
            .collect(Collectors.toSet());
    validArgs.add("page");
    validArgs.add("size");

}

public Set<String> getInvalidArgs(final Set<String> args){
    Set<String> invalidArgs=new HashSet<>(args);
    invalidArgs.removeAll(validArgs);
    return invalidArgs;



    }
   }

, which uses reflection to find fields which are annotated with the "@Queryable" annotation:

package com.widgetstore.api.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Queryable {
} 

Now mark the fields of your domain class which you want queryable with that annotation:

@Getter
@Setter
public class Widget {
    @Queryable 
    private String productGuid;
    @Queryable 
    private String serialNumber;

    private String manufacturer;

Now make sure the ArgsChecker bean is created at application startup:

@SpringBootApplication
public class StartWidgetApi{

public static void main(String[] args){
    SpringApplication.run(StartWidgetApi.class);
}


@Bean(name="widgetArgsChecker")
public ArgsChecker<Widget> widgetArgsChecker(){
    return new ArgsChecker<Widget>(Widget.class);
}

//Other ArgsCheckers of different types may be used by other controllers.
@Bean(name="fooArgsChecker")
public ArgsChecker<Foo> fooArgsChecker(){
    return new ArgsChecker<Foo>(Foo.class);
 }
}

Finally,

Define a @ControllerAdvice class which will listen for exceptions thrown by your application:

package com.widgetstore.api.exception;


import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@ControllerAdvice
@RequestMapping(produces = "application/json")
@ResponseBody
public class RestControllerAdvice {


    @ExceptionHandler(QueryParameterNotSupportedException.class)
    public ResponseEntity<Map<String,Object>> unrecogonizedParameter(final QueryParameterNotSupportedException e){
        Map<String,Object> errorInfo = new LinkedHashMap<>();
        errorInfo.put("timestamp",new Date());
        errorInfo.put("errorMessage",e.getMessage());
        errorInfo.put("allowableParameters",e.getValidArgs());
        return new ResponseEntity<Map<String, Object>>(errorInfo,HttpStatus.BAD_REQUEST);
    }



}

, and define the QueryParameterNotSupportedException:

import lombok.Getter;

import java.util.Set;

@Getter
public class QueryParameterNotSupportedException extends RuntimeException{
    private Set<String> invalidArgs;

    private Set<String> validArgs;

    public QueryParameterNotSupportedException(String msg, Set<String> invalidArgs, Set<String> validArgs){
        super(msg);
        this.invalidArgs=invalidArgs;
        this.validArgs=validArgs;
    }

}

Now, when the user hits /widgets?someUnknownField=abc&someOtherField=xyz he will get a json response along the lines of

 {"timestamp": 2017-01-10'T'blahblah, "errorMessage": "The user supplied query parameter(s) that are not supported: ["someUnknownField","someOtherField"]. See below for a list of allowed query parameters." ,
"allowableParameters": ["productGuid","serialNumber"]
}
mancini0
  • 4,285
  • 1
  • 29
  • 31
3

Add HttpServletRequest request in method parameters, do

String query = request.getQueryString()

in the method body and validate that.

eis
  • 51,991
  • 13
  • 150
  • 199
  • 1
    Unfortunately this did not work, as query would only be set if the query path contains `...?query=testvalue` – membersound Feb 20 '15 at 17:01
  • @membersound hm, I may have misunderstood. How about just using `HttpServletRequest request` in params and `String query = request.getQueryString()` in the method? – eis Feb 20 '15 at 18:10
  • Great. Updated answer accordingly. :) – eis Feb 21 '15 at 07:52
  • Instead of actually verifying by writing a code, is there some JSR 303 validation that can be used? or some other validation ? – Rajas Gujarathi Sep 26 '19 at 16:20
  • @Rajas not really, as you're usually meant to just ignore any unknown parameters and only validate the ones you actually use. For those parameters you use, sure, there are. – eis Sep 26 '19 at 18:00
  • HttpServletRequest return null – aswzen Oct 17 '19 at 04:08
  • @aswzen I'd quess your servlet is either not a bean in spring context, or your request is not a servlet request – eis Oct 17 '19 at 09:41
3

I wanted to share a different way since I found ametiste's answer too manual and mancini0's overly verbose.

Suppose, you use Spring framework validation API to bind your request parameters to an object ApiRequest.

@InitBinder
public void initBinder(WebDataBinder binder, WebRequest request) {
    binder.setAllowedFields(ApiRequest.getAllowedRequestParameters());
}

@RequestMapping("/api")
public String content(@Valid ApiRequest request, BindingResult bindingResult) {
    return request.getId();
}

With following ApiRequest definition.

public class ApiRequest {
    private String id;

    public static String[] getAllowedRequestParameters() {
        return new String[]{
            "id"
        };
    }

}

Then you can use the BindingResult to perform the check whether there were any unexpected request parameters, like so:

    String[] suppressedFields = bindingResult.getSuppressedFields();
    if (suppressedFields.length > 0) {
        // your code here
    }
  • Is there a way to return "400 Bad Request" without having to check `bindingResult` every time? – Dragas Nov 01 '19 at 21:14