Tuesday, May 29, 2018

What's the proper way to escape URL variables with Spring's RestTemplate when calling a Spring RestController?

Leave a Comment

When calling RestTemplate.exchange to do a get request, such as:

String foo = "fo+o"; String bar = "ba r"; restTemplate.exchange("http://example.com/?foo={foo}&bar={bar}", HttpMethod.GET, null, foo, bar) 

what's the proper to have the URL variables correctly escaped for the get request?

Specifically, how do I get pluses (+) correctly escaped because Spring is interpreting as spaces, so, I need to encode them.

I tried using UriComponentsBuilder like this:

String foo = "fo+o"; String bar = "ba r"; UriComponentsBuilder ucb = UriComponentsBuilder.fromUriString("http://example.com/?foo={foo}&bar={bar}"); System.out.println(ucb.build().expand(foo, bar).toUri()); System.out.println(ucb.build().expand(foo, bar).toString()); System.out.println(ucb.build().expand(foo, bar).toUriString()); System.out.println(ucb.build().expand(foo, bar).encode().toUri()); System.out.println(ucb.build().expand(foo, bar).encode().toString()); System.out.println(ucb.build().expand(foo, bar).encode().toUriString()); System.out.println(ucb.buildAndExpand(foo, bar).toUri()); System.out.println(ucb.buildAndExpand(foo, bar).toString()); System.out.println(ucb.buildAndExpand(foo, bar).toUriString()); System.out.println(ucb.buildAndExpand(foo, bar).encode().toUri()); System.out.println(ucb.buildAndExpand(foo, bar).encode().toString()); System.out.println(ucb.buildAndExpand(foo, bar).encode().toUriString()); 

and that printed:

http://example.com/?foo=fo+o&bar=ba%20r http://example.com/?foo=fo+o&bar=ba r http://example.com/?foo=fo+o&bar=ba r http://example.com/?foo=fo+o&bar=ba%20r http://example.com/?foo=fo+o&bar=ba%20r http://example.com/?foo=fo+o&bar=ba%20r http://example.com/?foo=fo+o&bar=ba%20r http://example.com/?foo=fo+o&bar=ba r http://example.com/?foo=fo+o&bar=ba r http://example.com/?foo=fo+o&bar=ba%20r http://example.com/?foo=fo+o&bar=ba%20r http://example.com/?foo=fo+o&bar=ba%20r 

The space is correctly escaped in some instances, but the plus is never escaped.

I also tried UriTemplate like this:

String foo = "fo+o"; String bar = "ba r"; UriTemplate uriTemplate = new UriTemplate("http://example.com/?foo={foo}&bar={bar}"); Map<String, String> vars = new HashMap<>(); vars.put("foo", foo); vars.put("bar", bar); URI uri = uriTemplate.expand(vars); System.out.println(uri); 

with the exact same result:

http://example.com/?foo=fo+o&bar=ba%20r 

5 Answers

Answers 1

Apparently, the correct way of doing this is by defining a factory and changing the encoding mode:

String foo = "fo+o"; String bar = "ba r"; DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY); URI uri = factory.uriString("http://example.com/?foo={foo}&bar={bar}").build(foo, bar); System.out.println(uri); 

That prints out:

http://example.com/?foo=fo%2Bo&bar=ba%20r 

This is documented here: https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#web-uri-encoding

Answers 2

I think your problem here is that RFC 3986, on which UriComponents and by extension UriTemplate are based, does not mandate the escaping of + in a query string.

The spec's view on this is simply:

sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"                   / "*" / "+" / "," / ";" / "="  pchar       = unreserved / pct-encoded / sub-delims / ":" / "@"  query       = *( pchar / "/" / "?" )  URI         = scheme ":" hier-part [ "?" query ] [ "#" fragment ] 

If your web framework (Spring MVC, for example!) is interpreting + as a space, then that is its decision and not required under the URI spec.

With reference to the above, you will also see that !$'()*+,; are not escaped by UriTemplate. = and & are escaped because Spring has taken an "opinionated" view of what a query string looks like -- a sequence of key=value pairs.

Likewise, #[] and whitespace are escaped because they are illegal in a query string under the spec.

Granted, none of this is likely to be any consolation to you if you just quite reasonably want your query parameters escaped!

To actually encode the query params so your web framework can tolerate them, you could use something like org.springframework.web.util.UriUtils.encode(foo, charset).

Answers 3

I'm starting to believe this is a bug and I reported here: https://jira.spring.io/browse/SPR-16860

Currently, my workaround is this:

String foo = "fo+o"; String bar = "ba r"; String uri = UriComponentsBuilder.     fromUriString("http://example.com/?foo={foo}&bar={bar}").     buildAndExpand(vars).toUriString(); uri = uri.replace("+", "%2B"); // This is the horrible hack. try {     return new URI(uriString); } catch (URISyntaxException e) {     throw new RuntimeException("UriComponentsBuilder generated an invalid URI.", e); } 

which is a horrible hack that might fail in some situations.

Answers 4

For this one I would still prefer the encoding to be resolved using a proper method instead of using a Hack like you did. I would just use something like below

String foo = "fo+o"; String bar = "ba r"; MyUriComponentsBuilder ucb = MyUriComponentsBuilder.fromUriString("http://example.com/?foo={foo}&bar={bar}");  UriComponents uriString = ucb.buildAndExpand(foo, bar); // http://example.com/?foo=fo%252Bo&bar=ba+r URI x = uriString.toUri();  // http://example.com/?foo=fo%2Bo&bar=ba+r String y = uriString.toUriString();  // http://example.com/?foo=fo%2Bo&bar=ba+r String z = uriString.toString(); 

And of course the class is like below

class MyUriComponentsBuilder extends UriComponentsBuilder {     protected UriComponentsBuilder originalBuilder;       public MyUriComponentsBuilder(UriComponentsBuilder builder) {         // TODO Auto-generated constructor stub         originalBuilder = builder;     }       public static MyUriComponentsBuilder fromUriString(String uri) {         return new MyUriComponentsBuilder(UriComponentsBuilder.fromUriString(uri));     }       @Override     public UriComponents buildAndExpand(Object... values) {         // TODO Auto-generated method stub         for (int i = 0; i< values.length; i ++) {             try {                 values[i] = URLEncoder.encode((String) values[i], StandardCharsets.UTF_8.toString());             } catch (UnsupportedEncodingException e) {                 // TODO Auto-generated catch block                 e.printStackTrace();             }         }         return originalBuilder.buildAndExpand(values);     }  } 

Still not a cleanest possible way but better then doing a hardcoded replace approach

Answers 5

You could use UriComponentsBuilder in Spring (org.springframework.web.util.UriComponentsBuilder)

String url = UriComponentsBuilder     .fromUriString("http://example.com/")     .queryParam("foo", "fo+o")     .queryParam("bar", "ba r")     .build().toUriString();  restTemplate.exchange(url , HttpMethod.GET, httpEntity); 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment