SpringBoot urlencodes plus sign in query parameters in an inconsistent manner

Roman Puchkovskiy :

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:

  1. Is this a bug in Spring Boot (or one of the components it uses)?
  2. If not, did I do something wrong?
  3. 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).

Eugen Covaci :

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.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=87077&siteId=1