Kolargol00
Kolargol00

Reputation: 1887

How to have a RestTemplate encode all characters with UriComponents and EncodingMode.VALUES_ONLY?

I want my REST client, using Spring Web's RestTemplate, to %-encode all special characters in URL parameters, not only illegal characters. Spring Web's documentation states that the encoding method can be changed by configuring the DefaultUriBuilderFactory used by RestTemplate with setEncodingMode(EncodingMode.VALUES_ONLY):

String baseUrl = "http://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.VALUES_ONLY);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

this should "apply UriUtils.encode(String, Charset) to each URI variable value" which in turn will "encode all characters that are either illegal, or have any reserved meaning, anywhere within a URI, as defined in RFC 3986".

I wrote the following test case to try and demonstrate that changing to EncodingMode.VALUES_ONLY does not have the desired effect. (executing it with dependencies org.springframework.boot:spring-boot-starter:2.0.3.RELEASE, org.springframework:spring-web:5.0.7.RELEASE, org.springframework.boot:spring-boot-starter-test:2.0.3.RELEASE)

package com.example.demo.encoding;

import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

import java.nio.charset.StandardCharsets;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;

@RunWith(SpringRunner.class)
@RestClientTest(DemoClient.class)
public class EncodingTest {
  @Autowired private MockRestServiceServer mockServer;
  @Autowired private DemoClient client;

  @Test
  public void encodeAllCharactersInParameter() {
    mockServer.expect(requestTo(encodedQueryUrl("https://host", "+:/")))
      .andExpect(method(HttpMethod.GET))
      .andRespond(withSuccess());
    client.request("https://host", "+:/");
    mockServer.verify();
  }

  private String encodedQueryUrl(final String baseUrl, final String parameter) {
    return String.format("%s?parameter=%s", baseUrl,
      UriUtils.encode(parameter, StandardCharsets.UTF_8));
  }
}

@Component
class DemoClient {
  private final RestTemplate restTemplate;

  public DemoClient(RestTemplateBuilder restTemplateBuilder) {
    DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
    factory.setEncodingMode(EncodingMode.VALUES_ONLY);
    restTemplateBuilder.uriTemplateHandler(factory);
    this.restTemplate = restTemplateBuilder.build();
  }

  public Object request(final String url, final String parameter) {
    UriComponents queryUrl = UriComponentsBuilder.fromHttpUrl(url)
      .queryParam("parameter", parameter).build().encode();
    return restTemplate.getForObject(queryUrl.toUri(), Object.class);
  }
}

This test fails with java.lang.AssertionError: Request URI expected:<https://host?parameter=%2B%3A%2F> but was:<https://host?parameter=+:/>. So what am I doing wrong? Is it a bug in Spring Framework or does MockRestServiceServer decode URLs before verifying expectations?

Upvotes: 1

Views: 7890

Answers (2)

Kolargol00
Kolargol00

Reputation: 1887

Corrected example:

package com.example.demo.encoding;

import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

import java.nio.charset.StandardCharsets;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;

@RunWith(SpringRunner.class)
@RestClientTest(DemoClient.class)
public class EncodingTest {
  @Autowired private MockRestServiceServer mockServer;
  @Autowired private DemoClient client;

  @Test
  public void encodeAllCharactersInParameter() {
    mockServer.expect(requestTo(encodedQueryUrl("https://host", "+:/")))
      .andExpect(method(HttpMethod.GET))
      .andRespond(withSuccess());
    client.request("https://host", "+:/");
    mockServer.verify();
  }

  private String encodedQueryUrl(final String baseUrl, final String parameter) {
    return String.format("%s?parameter=%s", baseUrl,
      UriUtils.encode(parameter, StandardCharsets.UTF_8));
  }
}

@Component
class DemoClient {
  private final RestTemplate restTemplate;

  public DemoClient(RestTemplateBuilder restTemplateBuilder) {
    DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
    factory.setEncodingMode(EncodingMode.VALUES_ONLY);
    this.restTemplate = restTemplateBuilder.uriTemplateHandler(factory).build();
  }

  public Object request(final String url, final String parameter) {
    String urlString = UriComponentsBuilder.fromHttpUrl(url)
      .queryParam("parameter", "{param}").build().toUriString();
    return restTemplate.getForObject(urlString, Object.class, parameter);
  }
}

Upvotes: 0

Rossen Stoyanchev
Rossen Stoyanchev

Reputation: 5008

Two issues in the example:

One, the request method prepares and encodes a java.net.URI externally, so the RestTemplate is not the one preparing it. You need to pass a URI template with a URI variable in it, so that the RestTemplate has a chance to prepare the URI and do the encoding. For example:

public Object request(final String url, final String parameter) {
    String urlString = UriComponentsBuilder.fromHttpUrl(url)
            .queryParam("parameter", "{param}")
            .build()
            .toUriString();
    return restTemplate.getForObject(urlString, Object.class, parameter);
}

Or simply have request take the URI template string:

public Object request(final String url) {
    return restTemplate.getForObject(url, Object.class, parameter);
}

// then invoke like this...
request("https://host?parameter={param}");

Two, the RestTemplateBuilder#uriTemplateHandler returns a new instance, so you need to use that for the configuration change to take effect:

DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
factory.setEncodingMode(EncodingMode.VALUES_ONLY);
restTemplateBuilder = restTemplateBuilder.uriTemplateHandler(factory); // <<<< see here
this.restTemplate = restTemplateBuilder.build();

It works as expected with the above changes.

Note that https://jira.spring.io/browse/SPR-17039 will make it easier to also achieve the same effect using UriComponentsBuilder, so check for updates there.

Upvotes: 1

Related Questions