Skip to content

Commit f58937e

Browse files
committed
Adds StripContextPath filter [webflux]
Signed-off-by: Garvit Joshi <garvitjoshi9@gmail.com>
1 parent 3217852 commit f58937e

File tree

6 files changed

+255
-0
lines changed

6 files changed

+255
-0
lines changed

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setrequestheader-factory.adoc[]
4242
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setresponseheader-factory.adoc[]
4343
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setstatus-factory.adoc[]
44+
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripcontextpath-factory.adoc[]
4445
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripprefix-factory.adoc[]
4546
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc[]
4647
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestsize-factory.adoc[]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[[stripcontextpath-gatewayfilter-factory]]
2+
= `StripContextPath` `GatewayFilter` Factory
3+
4+
The `StripContextPath` `GatewayFilter` factory removes the request context path before downstream path filters run.
5+
This is useful when a context path is present through `spring.webflux.base-path` or forwarded prefixes and you want filters such as `StripPrefix`, `RewritePath`, `SetPath`, or `PrefixPath` to operate on the application path instead.
6+
7+
The `StripContextPath` filter must be listed before any path-manipulating filters.
8+
The following example configures a `StripContextPath` `GatewayFilter`:
9+
10+
.application.yml
11+
[source,yaml]
12+
----
13+
spring:
14+
cloud:
15+
gateway:
16+
server:
17+
webflux:
18+
routes:
19+
- id: strip_context_path_route
20+
uri: https://example.org
21+
predicates:
22+
- Path=/name/**
23+
filters:
24+
- StripContextPath
25+
- StripPrefix=1
26+
----
27+
28+
With a context path of `/context`, a request to `/context/name/blue` first becomes `/name/blue`, and then `StripPrefix=1` sends `https://example.org/blue` downstream.

spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
import org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactory;
120120
import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory;
121121
import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory;
122+
import org.springframework.cloud.gateway.filter.factory.StripContextPathGatewayFilterFactory;
122123
import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory;
123124
import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory;
124125
import org.springframework.cloud.gateway.filter.factory.rewrite.GzipMessageBodyResolver;
@@ -724,6 +725,12 @@ public SetStatusGatewayFilterFactory setStatusGatewayFilterFactory() {
724725
return new SetStatusGatewayFilterFactory();
725726
}
726727

728+
@Bean
729+
@ConditionalOnEnabledFilter
730+
public StripContextPathGatewayFilterFactory stripContextPathGatewayFilterFactory() {
731+
return new StripContextPathGatewayFilterFactory();
732+
}
733+
727734
@Bean
728735
@ConditionalOnEnabledFilter
729736
public SaveSessionGatewayFilterFactory saveSessionGatewayFilterFactory() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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.filter.factory;
18+
19+
import reactor.core.publisher.Mono;
20+
21+
import org.springframework.cloud.gateway.filter.GatewayFilter;
22+
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
23+
import org.springframework.http.server.reactive.ServerHttpRequest;
24+
import org.springframework.util.StringUtils;
25+
import org.springframework.web.server.ServerWebExchange;
26+
27+
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
28+
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
29+
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;
30+
31+
/**
32+
* Strips the request context path before later path-manipulating filters run.
33+
*
34+
* @author Garvit Joshi
35+
*/
36+
public class StripContextPathGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
37+
38+
public GatewayFilter apply() {
39+
return apply(new Object());
40+
}
41+
42+
@Override
43+
public GatewayFilter apply(Object config) {
44+
return new GatewayFilter() {
45+
@Override
46+
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
47+
ServerHttpRequest request = exchange.getRequest();
48+
String contextPath = request.getPath().contextPath().value();
49+
if (!StringUtils.hasText(contextPath)) {
50+
return chain.filter(exchange);
51+
}
52+
53+
addOriginalRequestUrl(exchange, request.getURI());
54+
55+
String newPath = request.getPath().pathWithinApplication().value();
56+
if (!StringUtils.hasText(newPath)) {
57+
newPath = "/";
58+
}
59+
60+
ServerHttpRequest newRequest = request.mutate().contextPath("").path(newPath).build();
61+
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());
62+
63+
return chain.filter(exchange.mutate().request(newRequest).build());
64+
}
65+
66+
@Override
67+
public String toString() {
68+
return filterToStringCreator(StripContextPathGatewayFilterFactory.this).toString();
69+
}
70+
};
71+
}
72+
73+
}

spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ public void shouldInjectOnlyEnabledBuiltInFilters() {
108108
"spring.cloud.gateway.server.webflux.filter.rewrite-request-parameter.enabled=false",
109109
"spring.cloud.gateway.server.webflux.filter.set-status.enabled=false",
110110
"spring.cloud.gateway.server.webflux.filter.save-session.enabled=false",
111+
"spring.cloud.gateway.server.webflux.filter.strip-context-path.enabled=false",
111112
"spring.cloud.gateway.server.webflux.filter.strip-prefix.enabled=false",
112113
"spring.cloud.gateway.server.webflux.filter.request-header-to-request-uri.enabled=false",
113114
"spring.cloud.gateway.server.webflux.filter.request-size.enabled=false",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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.filter.factory;
18+
19+
import java.net.URI;
20+
import java.util.LinkedHashSet;
21+
22+
import org.junit.jupiter.api.Test;
23+
import org.mockito.ArgumentCaptor;
24+
import reactor.core.publisher.Mono;
25+
26+
import org.springframework.cloud.gateway.filter.GatewayFilter;
27+
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
28+
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
29+
import org.springframework.mock.web.server.MockServerWebExchange;
30+
import org.springframework.web.server.ServerWebExchange;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.mockito.Mockito.mock;
34+
import static org.mockito.Mockito.when;
35+
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;
36+
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
37+
38+
/**
39+
* @author Garvit Joshi
40+
*/
41+
public class StripContextPathGatewayFilterFactoryTests {
42+
43+
@Test
44+
public void stripContextPathFilterWorks() {
45+
MockServerHttpRequest request = request("/context/service/path", "/context");
46+
47+
ServerWebExchange exchange = filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(),
48+
MockServerWebExchange.from(request));
49+
50+
assertThat(exchange.getRequest().getURI()).hasPath("/service/path");
51+
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
52+
53+
URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
54+
assertThat(requestUrl).hasScheme("http").hasHost("localhost").hasNoPort().hasPath("/service/path");
55+
LinkedHashSet<URI> uris = exchange.getRequiredAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
56+
assertThat(uris).contains(request.getURI());
57+
}
58+
59+
@Test
60+
public void stripContextPathFilterToRootWorks() {
61+
ServerWebExchange exchange = filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(),
62+
MockServerWebExchange.from(request("/context", "/context")));
63+
64+
assertThat(exchange.getRequest().getURI()).hasPath("/");
65+
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
66+
}
67+
68+
@Test
69+
public void stripContextPathFilterWithoutContextPathIsNoOp() {
70+
ServerWebExchange exchange = filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(),
71+
MockServerWebExchange.from(MockServerHttpRequest.get("http://localhost/service/path").build()));
72+
73+
assertThat(exchange.getRequest().getURI()).hasPath("/service/path");
74+
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
75+
assertThat(exchange.getAttributes()).doesNotContainKey(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
76+
}
77+
78+
@Test
79+
public void stripContextPathThenStripPrefixWorks() {
80+
ServerWebExchange exchange = stripContextPathExchange("/context/service/blue");
81+
82+
exchange = filterAndGetExchange(new StripPrefixGatewayFilterFactory().apply(c -> c.setParts(1)), exchange);
83+
84+
assertThat(exchange.getRequest().getURI()).hasPath("/blue");
85+
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
86+
}
87+
88+
@Test
89+
public void stripContextPathThenRewritePathWorks() {
90+
ServerWebExchange exchange = stripContextPathExchange("/context/service/blue");
91+
92+
exchange = filterAndGetExchange(new RewritePathGatewayFilterFactory()
93+
.apply(c -> c.setRegexp("/service/(?<remaining>.*)").setReplacement("/${remaining}")), exchange);
94+
95+
assertThat(exchange.getRequest().getURI()).hasPath("/blue");
96+
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
97+
}
98+
99+
@Test
100+
public void stripContextPathThenPrefixPathWorks() {
101+
ServerWebExchange exchange = stripContextPathExchange("/context/get");
102+
103+
exchange = filterAndGetExchange(new PrefixPathGatewayFilterFactory().apply(c -> c.setPrefix("/prefix")),
104+
exchange);
105+
106+
assertThat(exchange.getRequest().getURI()).hasPath("/prefix/get");
107+
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
108+
}
109+
110+
@Test
111+
public void stripContextPathThenSetPathWorks() {
112+
ServerWebExchange exchange = stripContextPathExchange("/context/service/blue");
113+
114+
exchange = filterAndGetExchange(new SetPathGatewayFilterFactory().apply(c -> c.setTemplate("/blue")), exchange);
115+
116+
assertThat(exchange.getRequest().getURI()).hasPath("/blue");
117+
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
118+
}
119+
120+
@Test
121+
public void toStringFormat() {
122+
GatewayFilter filter = new StripContextPathGatewayFilterFactory().apply();
123+
assertThat(filter.toString()).contains("StripContextPath");
124+
}
125+
126+
private ServerWebExchange stripContextPathExchange(String path) {
127+
return filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(),
128+
MockServerWebExchange.from(request(path, "/context")));
129+
}
130+
131+
private MockServerHttpRequest request(String path, String contextPath) {
132+
return MockServerHttpRequest.get("http://localhost" + path).contextPath(contextPath).build();
133+
}
134+
135+
private ServerWebExchange filterAndGetExchange(GatewayFilter filter, ServerWebExchange exchange) {
136+
GatewayFilterChain filterChain = mock(GatewayFilterChain.class);
137+
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
138+
when(filterChain.filter(captor.capture())).thenReturn(Mono.empty());
139+
140+
filter.filter(exchange, filterChain);
141+
142+
return captor.getValue();
143+
}
144+
145+
}

0 commit comments

Comments
 (0)