Encoded Comma in URL is read as List in Spring
Asked Answered
P

2

11

I have a REST Component in Spring. Simplified, it looks like this:

@RequestMapping(value = "/route", method = RequestMethod.GET)
public Object thisIsTheMethod(@RequestParam(value = "value", required = false) List<String> values) {
    return OtherClass.doTheThing(values);
}

This is used to consume a list of Strings of arbitrary length. You could do it a number of ways:

localhost:8080/route?value=this&value=that

OR

localhost:8080/route?value=this,that

Now let's say I want to pass in one string which contains a comma: value,1. How would I go about doing that? Replacing the comma with %2C results in a list of 2 values ("value", "1"). Putting anything in quotes, or escaped quotes, has similar problems. It looks like it works when I have multiple parameters and use the multiple-values pattern, though not when I use the comma-separated pattern.

Photophilous answered 5/5, 2016 at 19:22 Comment(6)
Haven't tried it, but I think the following should work. Define a domain object that models your request objects (value in this case). Create a List of these objects as JSON, Base64 encode it, and send the GET request. On the Spring side, change the Class type of the Request Param to be a list of your domain objects.Itu
@Andonaeus: That way lies madness. That said, I've been considering it since I started this particular section of this project.Photophilous
Very similar yet also unsolved: https://mcmap.net/q/1158909/-spring-framework-does-not-map-list-request-parameter-correctly/2954288Wallraff
@Photophilous It is indeed not the best solution, but wouldn't be so bad if your domain object was literally a class that exposes a single String variable. I can't really think of another way to do it, but I will test that solution and reply.Itu
Ran into a bug due to this today. App breaks due to an internal REST call to a Spring service that takes in a list of items. The app behaves unexpectedly when the user enters a string containing a comma, since the internal REST service treats them as two separate items.Seasonseasonable
Some solutions are here #4999248Sari
F
3

You can set your Delimiter. The comma is just the default one used in org.springframework.boot.convert.DelimitedStringToCollectionConverter when it isn't set. If you wish, you can disable the Delimiter completely. For your code, it would look like this:

import org.springframework.boot.convert.Delimiter;

@RequestMapping(value = "/route", method = RequestMethod.GET)
public Object thisIsTheMethod(@Delimiter(Delimiter.None) @RequestParam(value = "value", required = false) List<String> values) {
    return OtherClass.doTheThing(values);
}
Fabio answered 23/9, 2022 at 14:58 Comment(2)
While this didn't exist when I asked the question, this is certainly the correct answer now and for Spring Boot 2.0+Photophilous
didn't really workTeflon
I
2

EDIT: I'm not sure why I bothered creating a domain object and going through the trouble of encoding and decoding the whole list. It's just as well that you can simply Base64 encode each parameter, and decide each parameter when read in.

As per my comment above, I was able to implement a solution. I am not particularly satisfied with the implementation, but it gets the job done. I would like to reiterate that there is almost certainly a better solution that can follow a more explicit approach with regards to the domain objects and the marshalling and unmarshalling of the values.

Assuming the list of values is formatted as a JSON list of value objects such as:

[{"value":"test,1"},{"value": "test,2"}]

The Base64 encoding is:

W3sidmFsdWUiOiJ0ZXN0LDEifSx7InZhbHVlIjogInRlc3QsMiJ9XQ==

My Controller is setup as:

@RequestMapping(value = "/test", method = RequestMethod.GET)
@ResponseBody
public String test(@RequestParam(value = "values", required = true) String values) throws JsonParseException, JsonMappingException, IOException{

    byte[] bValues = Base64.getDecoder().decode(values.getBytes());
    String json = new String(bValues);
    ObjectMapper mapper = new ObjectMapper();
    List<Value> myObjects = mapper.readValue(json, new TypeReference<List<Value>>(){});
    StringBuilder result = new StringBuilder();

    int i = 1;
    for (Value value : myObjects){
        result.append("value ");
        result.append(" ");
        result.append(i++);
        result.append(": ");
        result.append(value.getValue());
        result.append("<br>");
    }

    result.setLength(result.length() - 1);
    return result.length() > 0 ? result.toString() : "Test";
}

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

The Value class is:

public class Value implements Serializable{

private static final long serialVersionUID = -7342446805363029057L;

private String value;

public Value(){

}

public String getValue() {
    return value;
}

public void setValue(String value) {
    this.value = value;
}

}

The request is:

/test?values=W3sidmFsdWUiOiJ0ZXN0LDEifSx7InZhbHVlIjogInRlc3QsMiJ9XQ==

And the response is:

value 1: test,1
value 2: test,2
Itu answered 5/5, 2016 at 21:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.