I have a simple echo controller
@RestController
public class EchoController {
@GetMapping(path = "/param", produces = MediaType.TEXT_PLAIN_VALUE)
String echoParam(@RequestParam("p") String paramValue) {
return paramValue;
}
@GetMapping(path = "/path-variable/{val}", produces = MediaType.TEXT_PLAIN_VALUE)
String echoPathVariable(@PathVariable("val") String val) {
return val;
}
}
One of its methods echoes the value of a parameter it was presented; the second does the same with a value provided via URI.
I have the following tests:
@Autowired
private WebTestClient webTestClient;
@Test
public void rawPlus_inQueryParam() {
String value = "1+1";
String response = getValueEchoedThroughQueryParam(value);
assertThat(response, is(equalTo(value)));
}
@Test
public void urlencodedPlus_inQueryParam() {
String value = "1%2B1";
String response = getValueEchoedThroughQueryParam(value);
assertThat(response, is(equalTo(value)));
}
private String getValueEchoedThroughQueryParam(String value) {
return webTestClient.get()
.uri(builder -> {
return builder
.path("/param")
.queryParam("p", value)
.build();
})
.exchange()
.expectStatus().is2xxSuccessful()
.expectBody(String.class)
.returnResult()
.getResponseBody();
}
Both tests just send a string via the query parameter, read the response and assert that the content was echoed correctly.
First test fails:
java.lang.AssertionError:
Expected: is "1+1"
but: was "1 1"
Second test passes.
Looks like in the second test, the WebTestClient
url-encodes the value (actually, it just url-encodes the percent character), then the web-server url-decodes it, and everything is ok. But in the first test, the client does not url-encode the plus character, but the server does url-decode it, hence it gets a space character.
This looks like an inconsistency. I doubt I could do something stupid to cause it because everything works in the default mode; actually, the controller I show here is almost the only code the application has (Application class itself is default, I did not touch it after it was generated).
Just to compare: when passing the same data in URI, everything works correctly. The following tests pass:
@Test
public void rawPlus_inPathVariable() {
String value = "1+1";
String response = getValueEchoedThroughPathVariable(value);
assertThat(response, is(equalTo(value)));
}
@Test
public void urlencodedPlus_inPathVariable() {
String value = "1%2B1";
String response = getValueEchoedThroughPathVariable(value);
assertThat(response, is(equalTo(value)));
}
private String getValueEchoedThroughPathVariable(String value) {
return webTestClient.get()
.uri("/path-variable/" + value)
.exchange()
.expectStatus().is2xxSuccessful()
.expectBody(String.class)
.returnResult()
.getResponseBody();
}
The questions are:
- Is this a bug in Spring Boot (or one of the components it uses)?
- If not, did I do something wrong?
- And the practical question: how can you pass a query parameter value that contains plus character in tests written against
WebTestClient
? If I don't url-encode it manually, it gets url-decoded by the web-server; if I do, it gets url-encoded second time, so the server gets (after url-decoding) an url-decoded version.
Spring Boot version is 2.1.4.RELEASE
(the most recent at the moment of writing).
POM follows:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-plus-in-query-string</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-plus-in-query-string</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The project is available at GitHub: https://github.com/rpuch/springboot-plus-in-query-string
Just run the tests (mvn clean test
).
See this issue
The key to understand this, is that different degrees of encoding are applied to the URI template vs URI variables. In other words given:
http://example.com/a/{b}/c?q={q}&p={p}
The URI template is everything except for the URI variable placeholders. However the code snippet you showed only builds a URI literal without any variables, so the level encoding is the same, only illegal characters, no matter which method is used.
So it would also have to be something like:
.queryParam("foo", "{foo}").buildAndExpand(foo)
Therefore the idea is to use something like:
builder
.path("/param")
.queryParam("p", "{value}")
.build(value)
to get the query param encoded.