Search code examples
javaspringspring-bootresttemplatespring-resttemplate

Encoding a URL Query Parameter so it can have a '+'


Apparently, in the move from Spring Boot 1 to Spring Boot 2 (Spring 5), the encoding behavior of URL parameters for RestTemplates changed. It seems unusually difficult to get a general query parameter on rest templates passed so that characters that have special meanings such as "+" get properly escaped. It seems that, since "+" is a valid character, it doesn't get escaped, even though its meaning gets altered (see here). This seems bizarre, counter-intuitive, and against every other convention on every other platform. More importantly, I can't figure out how to easily get around it. If I encode the string first, it gets double-encoded, because the "%"s get re-encoded. Anyway, this seems like it should be something very simple that the framework does, but I'm not figuring it out.

Here is my code that worked in Spring Boot 1:

  String url = "https://base/url/here";
  UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
  for (Map.Entry<String, String> entry : query.entrySet()) {
    builder.queryParam(entry.getKey(), entry.getValue());
  }
  HttpEntity<TheResponse> resp = myRestTemplate.exchange(builder.toUriString(), ...);

However, now it won't encode the "+" character, so the other end is interpreting it as a space. What is the correct way to build this URL in Java Spring Boot 2?

Note - I also tried this, but it actually DOUBLE-encodes everything:

try {
  for (Map.Entry<String, String> entry : query.entrySet()) {
    builder.queryParam(entry.getKey(), URLEncoder.encode(entry.getValue(),"UTF-8" ));
  }
} catch(Exception e) {
  System.out.println("Encoding error");
}

In the first one, if I put in "q" => "abc+1@efx.com", then, exactly in the URL, I get "abc+1@efx.com" (i.e., not encoded at all). However, in the second one, if I put in "abc+1@efx.com", then I get "abc%252B1%2540efx.com", which is DOUBLE-encoded.

I could hand-write an encoding method, but this seems (a) like overkill, and (b) doing encoding yourself is where security problems and weird bugs tend to creep in. But it seems insane to me that you can't just add a query parameter in Spring Boot 2. That seems like a basic task. What am I missing?


Solution

  • Found what I believe to be a decent solution. It turns out that a large part of the problem is actually the "exchange" function, which takes a string for a URL, but then re-encodes that URL for reasons I cannot fathom. However, the exchange function can be sent a java.net.URI instead. In this case, it does not try to interpolate anything, as it is already a URI. I then use java.net.URLEncoder.encode() to encode the pieces. I still have no idea why this isn't standard in Spring, but this should work.

        private String mapToQueryString(Map<String, String> query) {
            List<String> entries = new LinkedList<String>();
            for (Map.Entry<String, String> entry : query.entrySet()) {
                try {
                    entries.add(URLEncoder.encode(entry.getKey(), "UTF-8") + "=" + URLEncoder.encode(entry.getValue(), "UTF-8"));
                } catch(Exception e) {
                    log.error("Unable to encode string for URL: " + entry.getKey() + " / " + entry.getValue(), e);
                }
            }
            return String.join("&", entries);
        }
    
        /* Later in the code */
        String endpoint = "https://baseurl.example.com/blah";
        String finalUrl = query.isEmpty() ? endpoint : endpoint + "?" + mapToQueryString(query);
        URI uri;
        try {
            uri = new URI(finalUrl);
        } catch(URISyntaxException e) {
            log.error("Bad URL // " + finalUrl, e);
                return null;
            }
        }
        /* ... */
        HttpEntity<TheResponse> resp = myRestTemplate.exchange(uri, ...)