Skip to content

Commit 9f00e47

Browse files
committed
fix: preserve query string params for multipart requests in Gateway MVC
Fixes gh-3220 Signed-off-by: Arindam Singh <96876969+ar249@users.noreply.github.com>
1 parent 15bd55a commit 9f00e47

2 files changed

Lines changed: 140 additions & 2 deletions

File tree

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,24 @@
1616

1717
package org.springframework.cloud.gateway.server.mvc.handler;
1818

19-
import java.util.Collections;
19+
import java.nio.charset.StandardCharsets;
2020
import java.util.Map;
21+
import java.util.stream.Collectors;
2122

2223
import jakarta.servlet.http.HttpServletRequest;
2324

2425
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
26+
import org.springframework.util.MultiValueMap;
2527
import org.springframework.web.multipart.MultipartException;
2628
import org.springframework.web.multipart.MultipartHttpServletRequest;
2729
import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;
2830
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
2931

3032
/**
3133
* A MultipartResolver that does not resolve if the current request is a Gateway request.
34+
*
35+
* @author Spencer Gibb
36+
* @author Arindam Singh
3237
*/
3338
public class GatewayMvcMultipartResolver extends StandardServletMultipartResolver {
3439

@@ -67,7 +72,13 @@ protected void initializeMultipart() {
6772
@Override
6873
public Map<String, String[]> getParameterMap() {
6974
if (isGatewayRequest(getRequest())) {
70-
return Collections.emptyMap();
75+
MultiValueMap<String, String> queryParams =
76+
MvcUtils.decodeQueryString(getRequest().getQueryString(), StandardCharsets.UTF_8);
77+
78+
return queryParams.entrySet()
79+
.stream()
80+
.collect(Collectors.toMap(
81+
Map.Entry::getKey, e -> e.getValue().toArray(new String[0])));
7182
}
7283
return super.getParameterMap();
7384
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2013-present 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.nio.charset.StandardCharsets;
20+
import java.util.Map;
21+
22+
import org.apache.commons.lang3.StringUtils;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
27+
import org.springframework.mock.web.MockHttpServletRequest;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.mockito.Mockito.never;
31+
import static org.mockito.Mockito.spy;
32+
import static org.mockito.Mockito.verify;
33+
34+
public class GatewayMvcMultipartResolverTest {
35+
private MockHttpServletRequest mockRequest;
36+
37+
@BeforeEach
38+
public void setUp() {
39+
mockRequest = new MockHttpServletRequest();
40+
mockRequest.setContentType("multipart/form-data; boundary=----boundary");
41+
}
42+
43+
private GatewayMvcMultipartResolver.GatewayMultipartHttpServletRequest buildWrapper() {
44+
return new GatewayMvcMultipartResolver.GatewayMultipartHttpServletRequest(mockRequest);
45+
}
46+
47+
private void makeGatewayRequest() {
48+
mockRequest.setAttribute(MvcUtils.GATEWAY_ROUTE_ID_ATTR, "my-route");
49+
}
50+
51+
@Test
52+
public void getParameterMapGatewayRequestMultipleDistinctParams() {
53+
makeGatewayRequest();
54+
mockRequest.setQueryString("foo=bar&baz=qux");
55+
56+
Map<String, String[]> result = buildWrapper().getParameterMap();
57+
58+
assertThat(result).containsKeys("foo", "baz");
59+
assertThat(result.get("foo")).containsExactly("bar");
60+
assertThat(result.get("baz")).containsExactly("qux");
61+
}
62+
63+
@Test
64+
public void getParameterMapGatewayRequestNullQueryStringReturnsEmptyMap() {
65+
makeGatewayRequest();
66+
mockRequest.setQueryString(null);
67+
68+
Map<String, String[]> result = buildWrapper().getParameterMap();
69+
70+
assertThat(result).isEmpty();
71+
}
72+
73+
@Test
74+
public void getParameterMapGatewayRequestEmptyQueryStringReturnsEmptyMap() {
75+
makeGatewayRequest();
76+
mockRequest.setQueryString(StringUtils.EMPTY);
77+
78+
Map<String, String[]> result = buildWrapper().getParameterMap();
79+
80+
assertThat(result).isEmpty();
81+
}
82+
83+
@Test
84+
public void getParameterMap_gatewayRequest_multipartBodyIsNotParsed() {
85+
makeGatewayRequest();
86+
87+
String body = """
88+
------TestBoundary\r
89+
Content-Disposition: form-data; name="file"; filename="test.txt"\r
90+
Content-Type: text/plain\r
91+
\r
92+
file content here\r
93+
------TestBoundary\r
94+
Content-Disposition: form-data; name="field1"\r
95+
\r
96+
value1\r
97+
------TestBoundary--\r
98+
""";
99+
100+
mockRequest.setContentType("multipart/form-data; boundary=----TestBoundary");
101+
mockRequest.setContent(body.getBytes(StandardCharsets.UTF_8));
102+
mockRequest.setQueryString("queryParam=fromQuery");
103+
104+
GatewayMvcMultipartResolver.GatewayMultipartHttpServletRequest wrapper =
105+
spy(new GatewayMvcMultipartResolver.GatewayMultipartHttpServletRequest(mockRequest));
106+
107+
Map<String, String[]> params = wrapper.getParameterMap();
108+
109+
// multipart parsing isn't triggered
110+
verify(wrapper, never()).initializeMultipart();
111+
112+
// Body parts must not appear — only query string should be visible
113+
assertThat(params).containsOnlyKeys("queryParam");
114+
assertThat(params.get("queryParam")).containsExactly("fromQuery");
115+
assertThat(params).doesNotContainKey("field1").doesNotContainKey("file");
116+
}
117+
118+
@Test
119+
public void getParameterMapNonGatewayRequestNoAttributesDelegatesToSuper() {
120+
mockRequest.setParameter("superKey", "superValue");
121+
122+
Map<String, String[]> result = buildWrapper().getParameterMap();
123+
124+
assertThat(result).containsKey("superKey");
125+
assertThat(result.get("superKey")).containsExactly("superValue");
126+
}
127+
}

0 commit comments

Comments
 (0)