Skip to content

Commit d12a587

Browse files
committed
Merge branch '4.2.x'
2 parents 931232b + 863ac7f commit d12a587

File tree

3 files changed

+166
-28
lines changed

3 files changed

+166
-28
lines changed

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java

Lines changed: 16 additions & 1 deletion
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.
@@ -21,6 +21,7 @@
2121
import java.io.InputStream;
2222
import java.io.UncheckedIOException;
2323
import java.net.URI;
24+
import java.nio.charset.StandardCharsets;
2425
import java.util.Arrays;
2526
import java.util.Collection;
2627
import java.util.Collections;
@@ -40,11 +41,14 @@
4041
import org.springframework.http.converter.HttpMessageConverter;
4142
import org.springframework.util.Assert;
4243
import org.springframework.util.CollectionUtils;
44+
import org.springframework.util.LinkedMultiValueMap;
45+
import org.springframework.util.MultiValueMap;
4346
import org.springframework.util.StreamUtils;
4447
import org.springframework.web.context.WebApplicationContext;
4548
import org.springframework.web.servlet.function.ServerRequest;
4649
import org.springframework.web.servlet.support.RequestContextUtils;
4750
import org.springframework.web.util.UriComponentsBuilder;
51+
import org.springframework.web.util.UriUtils;
4852

4953
import static org.springframework.web.servlet.function.RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
5054

@@ -268,6 +272,17 @@ public static void addOriginalRequestUrl(ServerRequest request, URI url) {
268272
urls.add(url);
269273
}
270274

275+
public static MultiValueMap<String, String> encodeQueryParams(MultiValueMap<String, String> params) {
276+
MultiValueMap<String, String> encodedQueryParams = new LinkedMultiValueMap<>(params.size());
277+
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
278+
for (String value : entry.getValue()) {
279+
encodedQueryParams.add(UriUtils.encode(entry.getKey(), StandardCharsets.UTF_8),
280+
UriUtils.encode(value, StandardCharsets.UTF_8));
281+
}
282+
}
283+
return CollectionUtils.unmodifiableMultiValueMap(encodedQueryParams);
284+
}
285+
271286
private record ByteArrayInputMessage(ServerRequest request, ByteArrayInputStream body) implements HttpInputMessage {
272287

273288
@Override

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

Lines changed: 7 additions & 27 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.
@@ -37,6 +37,9 @@
3737
import org.springframework.web.servlet.function.ServerResponse;
3838
import org.springframework.web.util.UriComponentsBuilder;
3939

40+
/**
41+
* @author raccoonback
42+
*/
4043
public class ProxyExchangeHandlerFunction
4144
implements HandlerFunction<ServerResponse>, ApplicationListener<ContextRefreshedEvent> {
4245

@@ -84,14 +87,14 @@ private void init() {
8487
@Override
8588
public ServerResponse handle(ServerRequest serverRequest) {
8689
URI uri = uriResolver.apply(serverRequest);
87-
boolean encoded = containsEncodedQuery(serverRequest.uri(), serverRequest.params());
90+
MultiValueMap<String, String> params = MvcUtils.encodeQueryParams(serverRequest.params());
8891
// @formatter:off
8992
URI url = UriComponentsBuilder.fromUri(serverRequest.uri())
9093
.scheme(uri.getScheme())
9194
.host(uri.getHost())
9295
.port(uri.getPort())
93-
.replaceQueryParams(serverRequest.params())
94-
.build(encoded)
96+
.replaceQueryParams(params)
97+
.build(true)
9598
.toUri();
9699
// @formatter:on
97100

@@ -131,29 +134,6 @@ private <REQUEST_OR_RESPONSE> HttpHeaders filterHeaders(List<?> filters, HttpHea
131134
return filtered;
132135
}
133136

134-
private static boolean containsEncodedQuery(URI uri, MultiValueMap<String, String> params) {
135-
String rawQuery = uri.getRawQuery();
136-
boolean encoded = (rawQuery != null && rawQuery.contains("%"))
137-
|| (uri.getRawPath() != null && uri.getRawPath().contains("%"));
138-
139-
// Verify if it is really fully encoded. Treat partial encoded as unencoded.
140-
if (encoded) {
141-
try {
142-
UriComponentsBuilder.fromUri(uri).replaceQueryParams(params).build(true);
143-
return true;
144-
}
145-
catch (IllegalArgumentException ignored) {
146-
if (log.isTraceEnabled()) {
147-
log.trace("Error in containsEncodedParts", ignored);
148-
}
149-
}
150-
151-
return false;
152-
}
153-
154-
return false;
155-
}
156-
157137
public interface URIResolver extends Function<ServerRequest, URI> {
158138

159139
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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.BeansException;
26+
import org.springframework.beans.factory.ObjectProvider;
27+
import org.springframework.cloud.gateway.server.mvc.common.AbstractProxyExchange;
28+
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
29+
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
30+
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.RequestHttpHeadersFilter;
31+
import org.springframework.cloud.gateway.server.mvc.filter.HttpHeadersFilter.ResponseHttpHeadersFilter;
32+
import org.springframework.http.HttpHeaders;
33+
import org.springframework.mock.web.MockHttpServletRequest;
34+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
35+
import org.springframework.web.servlet.function.ServerRequest;
36+
import org.springframework.web.servlet.function.ServerResponse;
37+
38+
import static org.assertj.core.api.Assertions.assertThat;
39+
40+
/**
41+
* @author raccoonback
42+
*/
43+
class ProxyExchangeHandlerFunctionTest {
44+
45+
@Test
46+
void keepOriginalEncodingOfQueryParameter() {
47+
TestProxyExchange proxyExchange = new TestProxyExchange();
48+
ProxyExchangeHandlerFunction function = new ProxyExchangeHandlerFunction(proxyExchange, new ObjectProvider<>() {
49+
@Override
50+
public RequestHttpHeadersFilter getObject() throws BeansException {
51+
return null;
52+
}
53+
54+
@Override
55+
public RequestHttpHeadersFilter getObject(Object... args) throws BeansException {
56+
return null;
57+
}
58+
59+
@Override
60+
public RequestHttpHeadersFilter getIfAvailable() throws BeansException {
61+
return null;
62+
}
63+
64+
@Override
65+
public RequestHttpHeadersFilter getIfUnique() throws BeansException {
66+
return null;
67+
}
68+
69+
@Override
70+
public Stream<RequestHttpHeadersFilter> orderedStream() {
71+
return Stream.of((httpHeaders, serverRequest) -> new HttpHeaders());
72+
}
73+
74+
}, new ObjectProvider<>() {
75+
76+
@Override
77+
public ResponseHttpHeadersFilter getObject() throws BeansException {
78+
return null;
79+
}
80+
81+
@Override
82+
public ResponseHttpHeadersFilter getObject(Object... args) throws BeansException {
83+
return null;
84+
}
85+
86+
@Override
87+
public ResponseHttpHeadersFilter getIfAvailable() throws BeansException {
88+
return null;
89+
}
90+
91+
@Override
92+
public ResponseHttpHeadersFilter getIfUnique() throws BeansException {
93+
return null;
94+
}
95+
96+
@Override
97+
public Stream<ResponseHttpHeadersFilter> orderedStream() {
98+
return Stream.of((httpHeaders, serverRequest) -> new HttpHeaders());
99+
100+
}
101+
});
102+
103+
function.onApplicationEvent(null);
104+
105+
MockHttpServletRequest servletRequest = MockMvcRequestBuilders
106+
.get("http://localhost/é?foo=value1 value2&bar=value3=&qux=value4+")
107+
.buildRequest(null);
108+
servletRequest.setAttribute(MvcUtils.GATEWAY_REQUEST_URL_ATTR, URI.create("http://localhost:8080"));
109+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
110+
111+
function.handle(request);
112+
113+
URI uri = proxyExchange.getRequest().getUri();
114+
115+
assertThat(uri).hasToString("http://localhost:8080/%C3%A9?foo=value1%20value2&bar=value3%3D&qux=value4%2B")
116+
.hasPath("/é")
117+
.hasParameter("foo", "value1 value2")
118+
.hasParameter("bar", "value3=")
119+
.hasParameter("qux", "value4+");
120+
}
121+
122+
private class TestProxyExchange extends AbstractProxyExchange {
123+
124+
private Request request;
125+
126+
protected TestProxyExchange() {
127+
super(new GatewayMvcProperties());
128+
}
129+
130+
@Override
131+
public ServerResponse exchange(Request request) {
132+
this.request = request;
133+
134+
return ServerResponse.ok().build();
135+
}
136+
137+
public Request getRequest() {
138+
return request;
139+
}
140+
141+
}
142+
143+
}

0 commit comments

Comments
 (0)