Skip to content

Commit f1f49e0

Browse files
committed
Improve ProxyExchangeHandlerFunction to preserve original encoding of query parameters during proxy request
ref. #3759 Signed-off-by: raccoonback <kosb15@naver.com>
1 parent c70b671 commit f1f49e0

File tree

2 files changed

+115
-4
lines changed

2 files changed

+115
-4
lines changed

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/ProxyExchangeHandlerFunction.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2023 the original author or authors.
2+
* Copyright 2013-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,7 +36,11 @@
3636
import org.springframework.web.servlet.function.ServerRequest;
3737
import org.springframework.web.servlet.function.ServerResponse;
3838
import org.springframework.web.util.UriComponentsBuilder;
39+
import org.springframework.web.util.UriUtils;
3940

41+
/**
42+
* @author raccoonback
43+
*/
4044
public class ProxyExchangeHandlerFunction
4145
implements HandlerFunction<ServerResponse>, ApplicationListener<ContextRefreshedEvent> {
4246

@@ -84,14 +88,14 @@ private void init() {
8488
@Override
8589
public ServerResponse handle(ServerRequest serverRequest) {
8690
URI uri = uriResolver.apply(serverRequest);
87-
boolean encoded = containsEncodedQuery(serverRequest.uri(), serverRequest.params());
91+
MultiValueMap<String, String> params = ensureEncodedQueryParameters(serverRequest);
8892
// @formatter:off
8993
URI url = UriComponentsBuilder.fromUri(serverRequest.uri())
9094
.scheme(uri.getScheme())
9195
.host(uri.getHost())
9296
.port(uri.getPort())
93-
.replaceQueryParams(serverRequest.params())
94-
.build(encoded)
97+
.replaceQueryParams(params)
98+
.build(true)
9599
.toUri();
96100
// @formatter:on
97101

@@ -131,6 +135,15 @@ private <REQUEST_OR_RESPONSE> HttpHeaders filterHeaders(List<?> filters, HttpHea
131135
return filtered;
132136
}
133137

138+
private static MultiValueMap<String, String> ensureEncodedQueryParameters(ServerRequest serverRequest) {
139+
boolean encoded = containsEncodedQuery(serverRequest.uri(), serverRequest.params());
140+
MultiValueMap<String, String> params = serverRequest.params();
141+
if (!encoded) {
142+
params = UriUtils.encodeQueryParams(serverRequest.params());
143+
}
144+
return params;
145+
}
146+
134147
private static boolean containsEncodedQuery(URI uri, MultiValueMap<String, String> params) {
135148
String rawQuery = uri.getRawQuery();
136149
boolean encoded = (rawQuery != null && rawQuery.contains("%"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.server.mvc.handler;
18+
19+
import java.net.URI;
20+
import java.util.Collections;
21+
import java.util.stream.Stream;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.beans.factory.ObjectProvider;
26+
import org.springframework.cloud.gateway.server.mvc.common.AbstractProxyExchange;
27+
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
28+
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
29+
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.RequestHttpHeadersFilter;
30+
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.ResponseHttpHeadersFilter;
31+
import org.springframework.http.HttpHeaders;
32+
import org.springframework.mock.web.MockHttpServletRequest;
33+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
34+
import org.springframework.web.servlet.function.ServerRequest;
35+
import org.springframework.web.servlet.function.ServerResponse;
36+
37+
import static org.assertj.core.api.Assertions.assertThat;
38+
39+
/**
40+
* @author raccoonback
41+
*/
42+
class ProxyExchangeHandlerFunctionTest {
43+
44+
@Test
45+
void keepOriginalEncodingOfQueryParameter() {
46+
TestProxyExchange proxyExchange = new TestProxyExchange();
47+
ProxyExchangeHandlerFunction function = new ProxyExchangeHandlerFunction(proxyExchange, new ObjectProvider<>() {
48+
@Override
49+
public Stream<RequestHttpHeadersFilter> stream() {
50+
return Stream.of((httpHeaders, serverRequest) -> new HttpHeaders());
51+
}
52+
}, new ObjectProvider<>() {
53+
@Override
54+
public Stream<ResponseHttpHeadersFilter> stream() {
55+
return Stream.of((httpHeaders, serverRequest) -> new HttpHeaders());
56+
}
57+
});
58+
59+
function.onApplicationEvent(null);
60+
61+
MockHttpServletRequest servletRequest = MockMvcRequestBuilders
62+
.get("http://localhost/é?foo=value1 value2&bar=value3=")
63+
.buildRequest(null);
64+
servletRequest.setAttribute(MvcUtils.GATEWAY_REQUEST_URL_ATTR, URI.create("http://localhost:8080"));
65+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
66+
67+
function.handle(request);
68+
69+
URI uri = proxyExchange.getRequest().getUri();
70+
71+
assertThat(uri).hasToString("http://localhost:8080/%C3%A9?foo=value1%20value2&bar=value3%3D")
72+
.hasPath("/é")
73+
.hasParameter("foo", "value1 value2")
74+
.hasParameter("bar", "value3=");
75+
}
76+
77+
private class TestProxyExchange extends AbstractProxyExchange {
78+
79+
private Request request;
80+
81+
protected TestProxyExchange() {
82+
super(new GatewayMvcProperties());
83+
}
84+
85+
@Override
86+
public ServerResponse exchange(Request request) {
87+
this.request = request;
88+
89+
return ServerResponse.ok().build();
90+
}
91+
92+
public Request getRequest() {
93+
return request;
94+
}
95+
96+
}
97+
98+
}

0 commit comments

Comments
 (0)