Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setrequestheader-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setresponseheader-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setstatus-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripcontextpath-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripprefix-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestsize-factory.adoc[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[[stripcontextpath-gatewayfilter-factory]]
= `StripContextPath` `GatewayFilter` Factory

The `StripContextPath` `GatewayFilter` factory removes the request context path before downstream path filters run.
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.

The `StripContextPath` filter must be listed before any path-manipulating filters.
The following example configures a `StripContextPath` `GatewayFilter`:

.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: strip_context_path_route
uri: https://example.org
predicates:
- Path=/name/**
filters:
- StripContextPath
- StripPrefix=1
----

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.
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
import org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.StripContextPathGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.GzipMessageBodyResolver;
Expand Down Expand Up @@ -724,6 +725,12 @@ public SetStatusGatewayFilterFactory setStatusGatewayFilterFactory() {
return new SetStatusGatewayFilterFactory();
}

@Bean
@ConditionalOnEnabledFilter
public StripContextPathGatewayFilterFactory stripContextPathGatewayFilterFactory() {
return new StripContextPathGatewayFilterFactory();
}

@Bean
@ConditionalOnEnabledFilter
public SaveSessionGatewayFilterFactory saveSessionGatewayFilterFactory() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2013-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.gateway.filter.factory;

import reactor.core.publisher.Mono;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;

import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;

/**
* Strips the request context path before later path-manipulating filters run.
*
* @author Garvit Joshi
*/
public class StripContextPathGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {

public GatewayFilter apply() {
return apply(new Object());
}

@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String contextPath = request.getPath().contextPath().value();
if (!StringUtils.hasText(contextPath)) {
return chain.filter(exchange);
}

addOriginalRequestUrl(exchange, request.getURI());

String newPath = request.getPath().pathWithinApplication().value();
if (!StringUtils.hasText(newPath)) {
newPath = "/";
}

ServerHttpRequest newRequest = request.mutate().contextPath("").path(newPath).build();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of setting things to an empty string. Does null work here instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried null, but it does not compile here. contextPath(...) is treated as non-null and NullAway fails the build with:

passing @Nullable parameter 'null' where @NonNull is required

So I kept contextPath("") for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, the field in the builder is nullable and in the constructor but not the builder method. @rstoyanchev is that an oversight?

exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());

return chain.filter(exchange.mutate().request(newRequest).build());
}

@Override
public String toString() {
return filterToStringCreator(StripContextPathGatewayFilterFactory.this).toString();
}
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public void shouldInjectOnlyEnabledBuiltInFilters() {
"spring.cloud.gateway.server.webflux.filter.rewrite-request-parameter.enabled=false",
"spring.cloud.gateway.server.webflux.filter.set-status.enabled=false",
"spring.cloud.gateway.server.webflux.filter.save-session.enabled=false",
"spring.cloud.gateway.server.webflux.filter.strip-context-path.enabled=false",
"spring.cloud.gateway.server.webflux.filter.strip-prefix.enabled=false",
"spring.cloud.gateway.server.webflux.filter.request-header-to-request-uri.enabled=false",
"spring.cloud.gateway.server.webflux.filter.request-size.enabled=false",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright 2013-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.gateway.filter.factory;

import java.net.URI;
import java.util.LinkedHashSet;

import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import reactor.core.publisher.Mono;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;

/**
* @author Garvit Joshi
*/
public class StripContextPathGatewayFilterFactoryTests {

@Test
public void stripContextPathFilterWorks() {
MockServerHttpRequest request = request("/context/service/path", "/context");

ServerWebExchange exchange = filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(),
MockServerWebExchange.from(request));

assertThat(exchange.getRequest().getURI()).hasPath("/service/path");
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();

URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
assertThat(requestUrl).hasScheme("http").hasHost("localhost").hasNoPort().hasPath("/service/path");
LinkedHashSet<URI> uris = exchange.getRequiredAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
assertThat(uris).contains(request.getURI());
}

@Test
public void stripContextPathFilterToRootWorks() {
ServerWebExchange exchange = filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(),
MockServerWebExchange.from(request("/context", "/context")));

assertThat(exchange.getRequest().getURI()).hasPath("/");
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
}

@Test
public void stripContextPathFilterWithoutContextPathIsNoOp() {
ServerWebExchange exchange = filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(),
MockServerWebExchange.from(MockServerHttpRequest.get("http://localhost/service/path").build()));

assertThat(exchange.getRequest().getURI()).hasPath("/service/path");
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
assertThat(exchange.getAttributes()).doesNotContainKey(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
}

@Test
public void stripContextPathThenStripPrefixWorks() {
ServerWebExchange exchange = stripContextPathExchange("/context/service/blue");

exchange = filterAndGetExchange(new StripPrefixGatewayFilterFactory().apply(c -> c.setParts(1)), exchange);

assertThat(exchange.getRequest().getURI()).hasPath("/blue");
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
}

@Test
public void stripContextPathThenRewritePathWorks() {
ServerWebExchange exchange = stripContextPathExchange("/context/service/blue");

exchange = filterAndGetExchange(new RewritePathGatewayFilterFactory()
.apply(c -> c.setRegexp("/service/(?<remaining>.*)").setReplacement("/${remaining}")), exchange);

assertThat(exchange.getRequest().getURI()).hasPath("/blue");
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
}

@Test
public void stripContextPathThenPrefixPathWorks() {
ServerWebExchange exchange = stripContextPathExchange("/context/get");

exchange = filterAndGetExchange(new PrefixPathGatewayFilterFactory().apply(c -> c.setPrefix("/prefix")),
exchange);

assertThat(exchange.getRequest().getURI()).hasPath("/prefix/get");
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
}

@Test
public void stripContextPathThenSetPathWorks() {
ServerWebExchange exchange = stripContextPathExchange("/context/service/blue");

exchange = filterAndGetExchange(new SetPathGatewayFilterFactory().apply(c -> c.setTemplate("/blue")), exchange);

assertThat(exchange.getRequest().getURI()).hasPath("/blue");
assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty();
}

@Test
public void toStringFormat() {
GatewayFilter filter = new StripContextPathGatewayFilterFactory().apply();
assertThat(filter.toString()).contains("StripContextPath");
}

private ServerWebExchange stripContextPathExchange(String path) {
return filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(),
MockServerWebExchange.from(request(path, "/context")));
}

private MockServerHttpRequest request(String path, String contextPath) {
return MockServerHttpRequest.get("http://localhost" + path).contextPath(contextPath).build();
}

private ServerWebExchange filterAndGetExchange(GatewayFilter filter, ServerWebExchange exchange) {
GatewayFilterChain filterChain = mock(GatewayFilterChain.class);
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
when(filterChain.filter(captor.capture())).thenReturn(Mono.empty());

filter.filter(exchange, filterChain);

return captor.getValue();
}

}
Loading