Reputation: 1887
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
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
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