Skip to content

Commit 3217852

Browse files
authored
Adds StripContextPath filter (#4089)
* Fix StripPrefix and RewritePath filters including servlet context-path fixes: #4088 Signed-off-by: Garvit Joshi <garvitjoshi9@gmail.com> * Add documentation for StripContextPath filter Signed-off-by: Garvit Joshi <garvitjoshi9@gmail.com> --------- Signed-off-by: Garvit Joshi <garvitjoshi9@gmail.com>
1 parent 55d71f3 commit 3217852

File tree

7 files changed

+237
-0
lines changed

7 files changed

+237
-0
lines changed

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
*** xref:spring-cloud-gateway-server-webmvc/filters/setrequestheader.adoc[]
105105
*** xref:spring-cloud-gateway-server-webmvc/filters/setresponseheader.adoc[]
106106
*** xref:spring-cloud-gateway-server-webmvc/filters/setstatus.adoc[]
107+
*** xref:spring-cloud-gateway-server-webmvc/filters/stripcontextpath.adoc[]
107108
*** xref:spring-cloud-gateway-server-webmvc/filters/stripprefix.adoc[]
108109
*** xref:spring-cloud-gateway-server-webmvc/filters/retry.adoc[]
109110
*** xref:spring-cloud-gateway-server-webmvc/filters/requestsize.adoc[]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
[[stripcontextpath-filter]]
2+
= `StripContextPath` Filter
3+
4+
The `StripContextPath` filter removes the servlet context-path (configured via `server.servlet.context-path`) from the request URI before downstream filters process it.
5+
This is useful when path-manipulating filters like `StripPrefix`, `RewritePath`, or `PrefixPath` should operate on the same path that route predicates match against, without the context-path prefix.
6+
7+
The `StripContextPath` filter must be placed before any path-manipulating filters in the filter chain.
8+
The following listing configures a `StripContextPath` filter:
9+
10+
.application.yml
11+
[source,yaml]
12+
----
13+
server:
14+
servlet:
15+
context-path: /context
16+
17+
spring:
18+
cloud:
19+
gateway:
20+
server:
21+
webmvc:
22+
routes:
23+
- id: strip_context_path_route
24+
uri: https://example.org
25+
predicates:
26+
- Path=/name/**
27+
filters:
28+
- StripContextPath
29+
- StripPrefix=1
30+
----
31+
32+
.GatewaySampleApplication.java
33+
[source,java]
34+
----
35+
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.stripContextPath;
36+
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.stripPrefix;
37+
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
38+
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
39+
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
40+
41+
@Configuration
42+
class RouteConfiguration {
43+
44+
@Bean
45+
public RouterFunction<ServerResponse> gatewayRouterFunctionsStripContextPath() {
46+
return route("strip_context_path_route")
47+
.GET("/name/**", http())
48+
.before(uri("https://example.org"))
49+
.before(stripContextPath())
50+
.before(stripPrefix(1))
51+
.build();
52+
}
53+
}
54+
----
55+
56+
With `server.servlet.context-path=/context`, when a request is made through the gateway to `/context/name/blue`, the `StripContextPath` filter removes the `/context` prefix, and then `StripPrefix` strips `/name`, resulting in a downstream request to `https://example.org/blue`.
57+
58+
WARNING: The `StripContextPath` filter must be listed before `StripPrefix`, `RewritePath`, `PrefixPath`, or any other filter that operates on the request path. Filters execute in the order they are defined, and these filters need the context-path already removed to work correctly.

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,25 @@ public static void addOriginalRequestUrl(ServerRequest request, URI url) {
320320
urls.add(url);
321321
}
322322

323+
/**
324+
* Removes the servlet context-path prefix from the given path. This ensures
325+
* path-manipulating filters (e.g., StripPrefix, RewritePath) operate on the same path
326+
* that route predicates match against.
327+
* @param request the server request used to obtain the context path
328+
* @param path the path to strip the context path from (raw or decoded)
329+
* @return the path without the servlet context-path prefix
330+
*/
331+
public static String stripContextPath(ServerRequest request, String path) {
332+
String contextPath = request.servletRequest().getContextPath();
333+
if (StringUtils.hasText(contextPath) && path.startsWith(contextPath)) {
334+
path = path.substring(contextPath.length());
335+
if (path.isEmpty()) {
336+
path = "/";
337+
}
338+
}
339+
return path;
340+
}
341+
323342
public static MultiValueMap<String, String> encodeQueryParams(MultiValueMap<String, String> params) {
324343
MultiValueMap<String, String> encodedQueryParams = new LinkedMultiValueMap<>(params.size());
325344
for (Map.Entry<String, List<String>> entry : params.entrySet()) {

spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,25 @@ public static Function<ServerRequest, ServerRequest> setRequestHostHeader(String
406406
};
407407
}
408408

409+
/**
410+
* Returns a filter function that strips the servlet context-path from the request
411+
* URI. Users should add this filter before path-manipulating filters (StripPrefix,
412+
* RewritePath, PrefixPath) when using {@code server.servlet.context-path}.
413+
* @return a function that strips the context-path from the request URI
414+
*/
415+
public static Function<ServerRequest, ServerRequest> stripContextPath() {
416+
return request -> {
417+
String rawPath = request.uri().getRawPath();
418+
String strippedPath = MvcUtils.stripContextPath(request, rawPath);
419+
if (!rawPath.equals(strippedPath)) {
420+
MvcUtils.addOriginalRequestUrl(request, request.uri());
421+
URI newUri = UriComponentsBuilder.fromUri(request.uri()).replacePath(strippedPath).build(true).toUri();
422+
return ServerRequest.from(request).uri(newUri).build();
423+
}
424+
return request;
425+
};
426+
}
427+
409428
public static Function<ServerRequest, ServerRequest> stripPrefix() {
410429
return stripPrefix(1);
411430
}

spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FilterFunctions.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@ static HandlerFilterFunction<ServerResponse, ServerResponse> setResponseHeader(S
206206
return ofResponseProcessor(AfterFilterFunctions.setResponseHeader(name, value));
207207
}
208208

209+
@Shortcut
210+
static HandlerFilterFunction<ServerResponse, ServerResponse> stripContextPath() {
211+
return ofRequestProcessor(BeforeFilterFunctions.stripContextPath());
212+
}
213+
209214
static HandlerFilterFunction<ServerResponse, ServerResponse> stripPrefix() {
210215
return stripPrefix(1);
211216
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.common;
18+
19+
import java.util.Collections;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.mock.web.MockHttpServletRequest;
24+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
25+
import org.springframework.web.servlet.function.ServerRequest;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* @author Garvit Joshi
31+
*/
32+
class MvcUtilsTests {
33+
34+
@Test
35+
void stripContextPathRemovesPrefix() {
36+
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/context/path")
37+
.buildRequest(null);
38+
servletRequest.setContextPath("/context");
39+
40+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
41+
42+
assertThat(MvcUtils.stripContextPath(request, "/context/path")).isEqualTo("/path");
43+
}
44+
45+
@Test
46+
void stripContextPathReturnsSlashWhenOnlyContextPath() {
47+
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/context")
48+
.buildRequest(null);
49+
servletRequest.setContextPath("/context");
50+
51+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
52+
53+
assertThat(MvcUtils.stripContextPath(request, "/context")).isEqualTo("/");
54+
}
55+
56+
@Test
57+
void stripContextPathNoOpWithoutContextPath() {
58+
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path").buildRequest(null);
59+
60+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
61+
62+
assertThat(MvcUtils.stripContextPath(request, "/path")).isEqualTo("/path");
63+
}
64+
65+
}

spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,76 @@ void stripPrefixWithPort() {
190190
assertThat(result.uri().toString()).hasToString("http://localhost:77/depth3");
191191
}
192192

193+
@Test
194+
void stripContextPath() {
195+
MockHttpServletRequest servletRequest = MockMvcRequestBuilders
196+
.get("http://localhost/context/depth1/depth2/depth3")
197+
.buildRequest(null);
198+
servletRequest.setContextPath("/context");
199+
200+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
201+
202+
ServerRequest result = BeforeFilterFunctions.stripContextPath().apply(request);
203+
204+
assertThat(result.uri().getRawPath()).isEqualTo("/depth1/depth2/depth3");
205+
}
206+
207+
@Test
208+
void stripContextPathWithoutContextPath() {
209+
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/depth1/depth2/depth3")
210+
.buildRequest(null);
211+
212+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
213+
214+
ServerRequest result = BeforeFilterFunctions.stripContextPath().apply(request);
215+
216+
assertThat(result.uri().getRawPath()).isEqualTo("/depth1/depth2/depth3");
217+
}
218+
219+
@Test
220+
void stripPrefixWithContextPath() {
221+
MockHttpServletRequest servletRequest = MockMvcRequestBuilders
222+
.get("http://localhost/context/depth1/depth2/depth3")
223+
.buildRequest(null);
224+
servletRequest.setContextPath("/context");
225+
226+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
227+
228+
ServerRequest stripped = BeforeFilterFunctions.stripContextPath().apply(request);
229+
ServerRequest result = BeforeFilterFunctions.stripPrefix(2).apply(stripped);
230+
231+
assertThat(result.uri().getRawPath()).isEqualTo("/depth3");
232+
}
233+
234+
@Test
235+
void rewritePathWithContextPath() {
236+
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/context/service/path")
237+
.buildRequest(null);
238+
servletRequest.setContextPath("/context");
239+
240+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
241+
242+
ServerRequest stripped = BeforeFilterFunctions.stripContextPath().apply(request);
243+
ServerRequest modified = BeforeFilterFunctions.rewritePath("/service/(?<remaining>.*)", "/${remaining}")
244+
.apply(stripped);
245+
246+
assertThat(modified.uri().getRawPath()).isEqualTo("/path");
247+
}
248+
249+
@Test
250+
void prefixPathWithContextPath() {
251+
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/context/get")
252+
.buildRequest(null);
253+
servletRequest.setContextPath("/context");
254+
255+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
256+
257+
ServerRequest stripped = BeforeFilterFunctions.stripContextPath().apply(request);
258+
ServerRequest modified = BeforeFilterFunctions.prefixPath("/prefix").apply(stripped);
259+
260+
assertThat(modified.uri().getRawPath()).isEqualTo("/prefix/get");
261+
}
262+
193263
@Test
194264
void stripPrefixWithEncodedPath() {
195265
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/depth1/depth2/depth3/é")

0 commit comments

Comments
 (0)