Using Spring-Boot 2.3.12, we followed the solution on https://github.com/spring-projects/spring-framework/issues/20211 and add

server.undertow.decode-url: false

to disable request decoding. The request mapping is:

/foo/{id}/bar/{value}

After the following GET request, the request reach to REST controller.

GET /foo/83115320/bar/test+%2B-%2F%3A%3D_

We would expect the value inside the controller is kept as-is due to disabled decoding: test+%2B-%2F%3A%3D_

However, we actually get something like the following which has been decoded: test++-/:=_

I debugged into the code and the decoding happens around org.springframework.web.method.annotation: Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);

the request URI inside webRequest has not changed.

Comment From: taoj-action

When running a unit test using @WebMvcTest(), the value is not decoded. Auto-decode only happens when running the application.

Comment From: quaff

Spring Boot use tomcat by default, please ensure you are using undertow.

Comment From: taoj-action

Yes, we are using undertow.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-undertow'
    implementation('org.springframework.boot:spring-boot-starter-web') {
        exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
    }

Comment From: taoj-action

Debug into Spring-Web UrPathHealper.java code, the problem comes in handling PathVariables. In order to allow special characters to be processed in our case, we have to set urlDecode to false to allow it to pass through the decodeRequestString call. However, when urlDecode is false, the following code decodes the path variable. So, there is no way to keep any of the path variable values passing through As-Is without decoding at all.

    /**
     * Decode the given URI path variables via {@link #decodeRequestString} unless
     * {@link #setUrlDecode} is set to {@code true} in which case it is assumed
     * the URL path from which the variables were extracted is already decoded
     * through a call to {@link #getLookupPathForRequest(HttpServletRequest)}.
     * @param request current HTTP request
     * @param vars the URI variables extracted from the URL path
     * @return the same Map or a new Map instance
     */
    public Map<String, String> decodePathVariables(HttpServletRequest request, Map<String, String> vars) {
        if (this.urlDecode) {
            return vars;
        }
        else {
            Map<String, String> decodedVars = CollectionUtils.newLinkedHashMap(vars.size());
            vars.forEach((key, value) -> decodedVars.put(key, decodeInternal(request, value)));
            return decodedVars;
        }
    }

Comment From: taoj-action

I found a workaround by customizing the UrlPathHelper.

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper() {
            @Override
            public Map<String, String> decodePathVariables(HttpServletRequest request, Map<String, String> vars) {
                return vars;
            }
        };
        urlPathHelper.setUrlDecode(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }

Although it is ideal, it works. I Hope the Spring community can come out with a better solution! Thanks!

Comment From: rstoyanchev

This is by design, for all types of controller method parameters, to be passed in decoded form. You can re-encode it with UriUtils if it's in a small number of places. Alternatively, you could also extend PathVariableMethodArgumentResolver to support a custom annotation such as @EncodedPathVariable. You can override the resolveName method to re-encode the value.