Improve ProxyExchangeHandlerFunction to preserve original encoding of query parameters during proxy request

ref. https://github.com/spring-cloud/spring-cloud-gateway/issues/3759

Signed-off-by: raccoonback <kosb15@naver.com>
This commit is contained in:
raccoonback
2025-05-07 23:00:14 +09:00
parent c70b671ed7
commit f1f49e0926
2 changed files with 115 additions and 4 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,7 +36,11 @@ import org.springframework.web.servlet.function.HandlerFunction;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* @author raccoonback
*/
public class ProxyExchangeHandlerFunction
implements HandlerFunction<ServerResponse>, ApplicationListener<ContextRefreshedEvent> {
@@ -84,14 +88,14 @@ public class ProxyExchangeHandlerFunction
@Override
public ServerResponse handle(ServerRequest serverRequest) {
URI uri = uriResolver.apply(serverRequest);
boolean encoded = containsEncodedQuery(serverRequest.uri(), serverRequest.params());
MultiValueMap<String, String> params = ensureEncodedQueryParameters(serverRequest);
// @formatter:off
URI url = UriComponentsBuilder.fromUri(serverRequest.uri())
.scheme(uri.getScheme())
.host(uri.getHost())
.port(uri.getPort())
.replaceQueryParams(serverRequest.params())
.build(encoded)
.replaceQueryParams(params)
.build(true)
.toUri();
// @formatter:on
@@ -131,6 +135,15 @@ public class ProxyExchangeHandlerFunction
return filtered;
}
private static MultiValueMap<String, String> ensureEncodedQueryParameters(ServerRequest serverRequest) {
boolean encoded = containsEncodedQuery(serverRequest.uri(), serverRequest.params());
MultiValueMap<String, String> params = serverRequest.params();
if (!encoded) {
params = UriUtils.encodeQueryParams(serverRequest.params());
}
return params;
}
private static boolean containsEncodedQuery(URI uri, MultiValueMap<String, String> params) {
String rawQuery = uri.getRawQuery();
boolean encoded = (rawQuery != null && rawQuery.contains("%"))

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.server.mvc.handler;
import java.net.URI;
import java.util.Collections;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.server.mvc.common.AbstractProxyExchange;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.RequestHttpHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.ResponseHttpHeadersFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author raccoonback
*/
class ProxyExchangeHandlerFunctionTest {
@Test
void keepOriginalEncodingOfQueryParameter() {
TestProxyExchange proxyExchange = new TestProxyExchange();
ProxyExchangeHandlerFunction function = new ProxyExchangeHandlerFunction(proxyExchange, new ObjectProvider<>() {
@Override
public Stream<RequestHttpHeadersFilter> stream() {
return Stream.of((httpHeaders, serverRequest) -> new HttpHeaders());
}
}, new ObjectProvider<>() {
@Override
public Stream<ResponseHttpHeadersFilter> stream() {
return Stream.of((httpHeaders, serverRequest) -> new HttpHeaders());
}
});
function.onApplicationEvent(null);
MockHttpServletRequest servletRequest = MockMvcRequestBuilders
.get("http://localhost/é?foo=value1 value2&bar=value3=")
.buildRequest(null);
servletRequest.setAttribute(MvcUtils.GATEWAY_REQUEST_URL_ATTR, URI.create("http://localhost:8080"));
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
function.handle(request);
URI uri = proxyExchange.getRequest().getUri();
assertThat(uri).hasToString("http://localhost:8080/%C3%A9?foo=value1%20value2&bar=value3%3D")
.hasPath("")
.hasParameter("foo", "value1 value2")
.hasParameter("bar", "value3=");
}
private class TestProxyExchange extends AbstractProxyExchange {
private Request request;
protected TestProxyExchange() {
super(new GatewayMvcProperties());
}
@Override
public ServerResponse exchange(Request request) {
this.request = request;
return ServerResponse.ok().build();
}
public Request getRequest() {
return request;
}
}
}