Skip to content

Commit 2dfc3f3

Browse files
authored
Merge pull request #3761 from stepancar/setrequesturi
Support dynamic paths in route URI using SetRequestUri filter
2 parents 5177890 + c9df303 commit 2dfc3f3

File tree

8 files changed

+321
-0
lines changed

8 files changed

+321
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[[seturi-gatewayfilter-factory]]
2+
= `SetRequestUri` `GatewayFilter` Factory
3+
4+
The `SetRequestUri` `GatewayFilter` factory takes a `uri` parameter.
5+
It offers a simple way to manipulate the request uri by allowing templated segments of the path.
6+
This uses the URI templates from Spring Framework.
7+
Multiple matching segments are allowed.
8+
The following listing configures a `SetRequestUri` `GatewayFilter`:
9+
10+
.application.yml
11+
[source,yaml]
12+
----
13+
spring:
14+
cloud:
15+
gateway:
16+
routes:
17+
- id: seturi_route
18+
uri: no://op
19+
predicates:
20+
- Path=/{appId}/**
21+
filters:
22+
- SetRequestUri=http://{appId}.example.com
23+
----
24+
25+
For a request path of `/red-application/blue`, this sets the uri to `http://red-application.example.com` before making the downstream request and the final url, including path is going to be `http://red-application.example.com/red-application/blue`
26+

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory;
112112
import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory;
113113
import org.springframework.cloud.gateway.filter.factory.SetRequestHostHeaderGatewayFilterFactory;
114+
import org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactory;
114115
import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory;
115116
import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory;
116117
import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory;
@@ -722,6 +723,12 @@ public RequestHeaderToRequestUriGatewayFilterFactory requestHeaderToRequestUriGa
722723
return new RequestHeaderToRequestUriGatewayFilterFactory();
723724
}
724725

726+
@Bean
727+
@ConditionalOnEnabledFilter
728+
public SetRequestUriGatewayFilterFactory setRequestUriGatewayFilterFactory() {
729+
return new SetRequestUriGatewayFilterFactory();
730+
}
731+
725732
@Bean
726733
@ConditionalOnEnabledFilter
727734
public RequestSizeGatewayFilterFactory requestSizeGatewayFilterFactory() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2013-2020 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.Arrays;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Optional;
24+
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import org.springframework.cloud.gateway.filter.GatewayFilter;
29+
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
30+
import org.springframework.web.server.ServerWebExchange;
31+
import org.springframework.web.util.UriComponentsBuilder;
32+
33+
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
34+
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.getUriTemplateVariables;
35+
36+
/**
37+
* This filter changes the request uri.
38+
*
39+
* @author Stepan Mikhailiuk
40+
*/
41+
public class SetRequestUriGatewayFilterFactory
42+
extends AbstractChangeRequestUriGatewayFilterFactory<SetRequestUriGatewayFilterFactory.Config> {
43+
44+
private static final Logger log = LoggerFactory.getLogger(SetRequestUriGatewayFilterFactory.class);
45+
46+
public SetRequestUriGatewayFilterFactory() {
47+
super(Config.class);
48+
}
49+
50+
@Override
51+
public List<String> shortcutFieldOrder() {
52+
return Arrays.asList(NAME_KEY);
53+
}
54+
55+
@Override
56+
public GatewayFilter apply(Config config) {
57+
// AbstractChangeRequestUriGatewayFilterFactory.apply() returns
58+
// OrderedGatewayFilter
59+
OrderedGatewayFilter gatewayFilter = (OrderedGatewayFilter) super.apply(config);
60+
return new OrderedGatewayFilter(gatewayFilter, gatewayFilter.getOrder()) {
61+
@Override
62+
public String toString() {
63+
return filterToStringCreator(SetRequestUriGatewayFilterFactory.this)
64+
.append("template", config.getTemplate())
65+
.toString();
66+
}
67+
};
68+
}
69+
70+
String getUri(ServerWebExchange exchange, Config config) {
71+
String template = config.getTemplate();
72+
73+
if (template.indexOf('{') == -1) {
74+
return template;
75+
}
76+
77+
Map<String, String> variables = getUriTemplateVariables(exchange);
78+
return UriComponentsBuilder.fromUriString(template).build().expand(variables).toUriString();
79+
}
80+
81+
@Override
82+
protected Optional<URI> determineRequestUri(ServerWebExchange exchange, Config config) {
83+
try {
84+
String url = getUri(exchange, config);
85+
URI uri = URI.create(url);
86+
if (!uri.isAbsolute()) {
87+
log.info("Request url is invalid: url={}, error=URI is not absolute", url);
88+
return Optional.ofNullable(null);
89+
}
90+
return Optional.of(uri);
91+
}
92+
catch (IllegalArgumentException e) {
93+
log.info("Request url is invalid : url={}, error={}", config.getTemplate(), e.getMessage());
94+
return Optional.ofNullable(null);
95+
}
96+
}
97+
98+
public static class Config {
99+
100+
private String template;
101+
102+
public String getTemplate() {
103+
return template;
104+
}
105+
106+
public void setTemplate(String template) {
107+
this.template = template;
108+
}
109+
110+
}
111+
112+
}

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory;
5959
import org.springframework.cloud.gateway.filter.factory.RequestHeaderSizeGatewayFilterFactory;
6060
import org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactory;
61+
import org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactory;
6162
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
6263
import org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory;
6364
import org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory;
@@ -869,6 +870,16 @@ public GatewayFilterSpec requestHeaderToRequestUri(String headerName) {
869870
return filter(getBean(RequestHeaderToRequestUriGatewayFilterFactory.class).apply(c -> c.setName(headerName)));
870871
}
871872

873+
/**
874+
* A filter which changes the URI the request will be routed to by the Gateway by
875+
* pulling it from a header on the request.
876+
* @param uri the URI
877+
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
878+
*/
879+
public GatewayFilterSpec setRequestUri(String uri) {
880+
return filter(getBean(SetRequestUriGatewayFilterFactory.class).apply(c -> c.setTemplate(uri)));
881+
}
882+
872883
/**
873884
* A filter which change the URI the request will be routed to by the Gateway.
874885
* @param determineRequestUri a {@link Function} which takes a

spring-cloud-gateway-server/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
@@ -100,6 +100,7 @@ public void shouldInjectOnlyEnabledBuiltInFilters() {
100100
"spring.cloud.gateway.server.webflux.filter.secure-headers.enabled=false",
101101
"spring.cloud.gateway.server.webflux.filter.set-request-header.enabled=false",
102102
"spring.cloud.gateway.server.webflux.filter.set-request-host-header.enabled=false",
103+
"spring.cloud.gateway.server.webflux.filter.set-request-uri.enabled=false",
103104
"spring.cloud.gateway.server.webflux.filter.set-response-header.enabled=false",
104105
"spring.cloud.gateway.server.webflux.filter.rewrite-response-header.enabled=false",
105106
"spring.cloud.gateway.server.webflux.filter.rewrite-location-response-header.enabled=false",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2013-2020 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 org.junit.jupiter.api.Test;
20+
21+
import org.springframework.boot.SpringBootConfiguration;
22+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
23+
import org.springframework.boot.test.context.SpringBootTest;
24+
import org.springframework.boot.test.web.server.LocalServerPort;
25+
import org.springframework.cloud.gateway.route.RouteLocator;
26+
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
27+
import org.springframework.cloud.gateway.test.BaseWebClientTests;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Import;
30+
import org.springframework.test.annotation.DirtiesContext;
31+
32+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
33+
34+
/**
35+
* @author Stepan Mikhailiuk
36+
*/
37+
@SpringBootTest(webEnvironment = RANDOM_PORT)
38+
@DirtiesContext
39+
public class SetRequestUriGatewayFilterFactoryIntegrationTests extends BaseWebClientTests {
40+
41+
@LocalServerPort
42+
int port;
43+
44+
@Test
45+
public void setUriWorkWithProperties() {
46+
testClient.get().uri("/").header("Host", "testservice.setrequesturi.org").exchange().expectStatus().isOk();
47+
48+
testClient.get()
49+
.uri("/service/testservice")
50+
.header("Host", "setrequesturi.org")
51+
.exchange()
52+
.expectStatus()
53+
.isOk();
54+
}
55+
56+
@EnableAutoConfiguration
57+
@SpringBootConfiguration
58+
@Import(DefaultTestConfig.class)
59+
public static class TestConfig {
60+
61+
@Bean
62+
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
63+
return builder.routes()
64+
.route("map_subdomain_to_service_name",
65+
r -> r.host("{serviceName}.setrequesturi.org")
66+
.filters(f -> f.prefixPath("/httpbin").setRequestUri("lb://{serviceName}"))
67+
.uri("no://op"))
68+
.route("map_path_to_service_name",
69+
r -> r.host("setrequesturi.org")
70+
.and()
71+
.path("/service/{serviceName}")
72+
.filters(f -> f.rewritePath("/.*", "/").setRequestUri("lb://{serviceName}"))
73+
.uri("no://op"))
74+
.build();
75+
}
76+
77+
}
78+
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2013-2020 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+
21+
import org.junit.jupiter.api.Test;
22+
import org.mockito.ArgumentCaptor;
23+
import reactor.core.publisher.Mono;
24+
25+
import org.springframework.cloud.gateway.filter.GatewayFilter;
26+
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
27+
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
28+
import org.springframework.mock.web.server.MockServerWebExchange;
29+
import org.springframework.web.server.ServerWebExchange;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.mockito.Mockito.mock;
33+
import static org.mockito.Mockito.when;
34+
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
35+
36+
/**
37+
* @author Stepan Mikhailiuk
38+
*/
39+
public class SetRequestUriGatewayFilterFactoryTests {
40+
41+
@Test
42+
public void filterChangeRequestUri() {
43+
SetRequestUriGatewayFilterFactory factory = new SetRequestUriGatewayFilterFactory();
44+
GatewayFilter filter = factory.apply(c -> c.setTemplate("https://example.com"));
45+
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost").build();
46+
ServerWebExchange exchange = MockServerWebExchange.from(request);
47+
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, URI.create("http://localhost"));
48+
GatewayFilterChain filterChain = mock(GatewayFilterChain.class);
49+
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
50+
when(filterChain.filter(captor.capture())).thenReturn(Mono.empty());
51+
filter.filter(exchange, filterChain);
52+
ServerWebExchange webExchange = captor.getValue();
53+
URI uri = (URI) webExchange.getAttributes().get(GATEWAY_REQUEST_URL_ATTR);
54+
assertThat(uri).isNotNull();
55+
assertThat(uri.toString()).isEqualTo("https://example.com");
56+
}
57+
58+
@Test
59+
public void filterDoesNotChangeRequestUriIfUriIsInvalid() throws Exception {
60+
SetRequestUriGatewayFilterFactory factory = new SetRequestUriGatewayFilterFactory();
61+
GatewayFilter filter = factory.apply(c -> c.setTemplate("invalid_uri"));
62+
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost").build();
63+
ServerWebExchange exchange = MockServerWebExchange.from(request);
64+
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, URI.create("http://localhost"));
65+
GatewayFilterChain filterChain = mock(GatewayFilterChain.class);
66+
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
67+
when(filterChain.filter(captor.capture())).thenReturn(Mono.empty());
68+
filter.filter(exchange, filterChain);
69+
ServerWebExchange webExchange = captor.getValue();
70+
URI uri = (URI) webExchange.getAttributes().get(GATEWAY_REQUEST_URL_ATTR);
71+
assertThat(uri).isNotNull();
72+
assertThat(uri.toURL().toString()).isEqualTo("http://localhost");
73+
}
74+
75+
@Test
76+
public void toStringFormat() {
77+
SetRequestUriGatewayFilterFactory.Config config = new SetRequestUriGatewayFilterFactory.Config();
78+
config.setTemplate("http://localhost:8080");
79+
GatewayFilter filter = new SetRequestUriGatewayFilterFactory().apply(config);
80+
assertThat(filter.toString()).contains("http://localhost:8080");
81+
}
82+
83+
}

spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
RewriteLocationResponseHeaderGatewayFilterFactoryTests.class,
6767
org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactoryTests.class,
6868
org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactoryIntegrationTests.class,
69+
org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactoryTests.class,
70+
org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactoryIntegrationTests.class,
6971
org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactoryTests.class,
7072
org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactoryTests.class,
7173
org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactoryIntegrationTests.class,

0 commit comments

Comments
 (0)