Adds ability to expand uri variables for other filters. (#711)

Currently only SetPath can use the uri variables from the path and host
predicates. Now, other filters can use them thru the
ServerWebExchangeUtils.expand() method.

fixes gh-704
This commit is contained in:
Spencer Gibb
2019-07-23 12:48:25 -04:00
committed by GitHub
parent 1cf449c870
commit f1dbcdc0fe
14 changed files with 242 additions and 24 deletions

View File

@@ -341,6 +341,23 @@ spring:
This will add `X-Request-Foo:Bar` header to the downstream request's headers for all matching requests.
AddRequestHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.
.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
predicates:
- Path=/foo/{segment}
filters:
- AddRequestHeader=X-Request-Foo, Bar-{segment}
----
=== AddRequestParameter GatewayFilter Factory
The AddRequestParameter GatewayFilter Factory takes a name and value parameter.
@@ -359,6 +376,23 @@ spring:
This will add `foo=bar` to the downstream request's query string for all matching requests.
AddRequestParameter is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.
.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- AddRequestParameter=foo, bar-{segment}
----
=== AddResponseHeader GatewayFilter Factory
The AddResponseHeader GatewayFilter Factory takes a name and value parameter.
@@ -377,6 +411,23 @@ spring:
This will add `X-Response-Foo:Bar` header to the downstream response's headers for all matching requests.
AddResponseHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.
.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- AddResponseHeader=foo, bar-{segment}
----
=== DedupeResponseHeader GatewayFilter Factory
The DedupeResponseHeader GatewayFilter Factory takes a `name` parameter and an optional `strategy` parameter. `name` can contain a list of header names, space separated.
@@ -839,6 +890,41 @@ spring:
For a request path of `/foo/bar`, this will set the path to `/bar` before making the downstream request.
=== SetRequestHeader GatewayFilter Factory
The SetRequestHeader GatewayFilter Factory takes `name` and `value` parameters.
.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: setrequestheader_route
uri: https://example.org
filters:
- SetRequestHeader=X-Request-Foo, Bar
----
This GatewayFilter replaces all headers with the given name, rather than adding. So if the downstream server responded with a `X-Request-Foo:1234`, this would be replaced with `X-Request-Foo:Bar`, which is what the downstream service would receive.
SetRequestHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.
.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: setrequestheader_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- SetRequestHeader=foo, bar-{segment}
----
=== SetResponseHeader GatewayFilter Factory
The SetResponseHeader GatewayFilter Factory takes `name` and `value` parameters.
@@ -857,6 +943,23 @@ spring:
This GatewayFilter replaces all headers with the given name, rather than adding. So if the downstream server responded with a `X-Response-Foo:1234`, this would be replaced with `X-Response-Foo:Bar`, which is what the gateway client would receive.
SetResponseHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.
.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: setresponseheader_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- SetResponseHeader=foo, bar-{segment}
----
=== SetStatus GatewayFilter Factory
The SetStatus GatewayFilter Factory takes a single `status` parameter. It must be a valid Spring `HttpStatus`. It may be the integer value `404` or the string representation of the enumeration `NOT_FOUND`.

View File

@@ -20,6 +20,7 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
@@ -37,8 +38,9 @@ public class AddRequestHeaderGatewayFilterFactory
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
ServerHttpRequest request = exchange.getRequest().mutate()
.header(config.getName(), config.getValue()).build();
.header(config.getName(), value).build();
return chain.filter(exchange.mutate().request(request).build());
}

View File

@@ -22,6 +22,7 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
@@ -52,10 +53,11 @@ public class AddRequestParameterGatewayFilterFactory
}
}
String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
// TODO urlencode?
query.append(config.getName());
query.append('=');
query.append(config.getValue());
query.append(value);
try {
URI newUri = UriComponentsBuilder.fromUri(uri)

View File

@@ -20,6 +20,7 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
@@ -36,8 +37,8 @@ public class AddResponseHeaderGatewayFilterFactory
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
exchange.getResponse().getHeaders().add(config.getName(),
config.getValue());
String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
exchange.getResponse().getHeaders().add(config.getName(), value);
return chain.filter(exchange);
}

View File

@@ -20,6 +20,7 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
@@ -37,9 +38,9 @@ public class SetRequestHeaderGatewayFilterFactory
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(
httpHeaders -> httpHeaders.set(config.name, config.value))
.headers(httpHeaders -> httpHeaders.set(config.name, value))
.build();
return chain.filter(exchange.mutate().request(request).build());

View File

@@ -20,6 +20,7 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
@@ -36,8 +37,9 @@ public class SetResponseHeaderGatewayFilterFactory
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
return chain.filter(exchange).then(Mono.fromRunnable(() -> exchange
.getResponse().getHeaders().set(config.name, config.value)));
.getResponse().getHeaders().set(config.name, value)));
}
@Override

View File

@@ -249,6 +249,19 @@ public final class ServerWebExchangeUtils {
return AsyncPredicate.from(predicate);
}
public static String expand(ServerWebExchange exchange, String template) {
Assert.notNull(exchange, "exchange may not be null");
Assert.notNull(template, "template may not be null");
if (template.indexOf('{') == -1) { // short circuit
return template;
}
Map<String, String> variables = getUriTemplateVariables(exchange);
return UriComponentsBuilder.fromPath(template).build().expand(variables)
.getPath();
}
@SuppressWarnings("unchecked")
public static void putUriTemplateVariables(ServerWebExchange exchange,
Map<String, String> uriVariables) {

View File

@@ -66,7 +66,7 @@ public class AddRequestHeaderGatewayFilterFactoryTests extends BaseWebClientTest
.exchange().expectBody(Map.class).consumeWith(result -> {
Map<String, Object> headers = getMap(result.getResponseBody(),
"headers");
assertThat(headers).containsEntry("X-Request-Acme", "ValueB");
assertThat(headers).containsEntry("X-Request-Acme", "ValueB-www");
});
}
@@ -89,9 +89,9 @@ public class AddRequestHeaderGatewayFilterFactoryTests extends BaseWebClientTest
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("add_request_header_java_test",
r -> r.path("/headers").and().host("**.addrequestheaderjava.org")
r -> r.path("/headers").and().host("{sub}.addrequestheaderjava.org")
.filters(f -> f.prefixPath("/httpbin")
.addRequestHeader("X-Request-Acme", "ValueB"))
.addRequestHeader("X-Request-Acme", "ValueB-{sub}"))
.uri(uri))
.build();
}

View File

@@ -68,7 +68,7 @@ public class AddRequestParameterGatewayFilterFactoryTests extends BaseWebClientT
@Test
public void addRequestParameterFilterWorksEncodedQueryJavaDsl() {
testRequestParameterFilter("www.addreqparamjava.org", "ValueB", "javaname",
testRequestParameterFilter("www.addreqparamjava.org", "ValueB-www", "javaname",
"%E6%89%8E%E6%A0%B9");
}
@@ -129,9 +129,9 @@ public class AddRequestParameterGatewayFilterFactoryTests extends BaseWebClientT
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("add_request_param_java_test",
r -> r.path("/get").and().host("**.addreqparamjava.org")
r -> r.path("/get").and().host("{sub}.addreqparamjava.org")
.filters(f -> f.prefixPath("/httpbin")
.addRequestParameter("example", "ValueB"))
.addRequestParameter("example", "ValueB-{sub}"))
.uri(uri))
.build();
}

View File

@@ -59,7 +59,7 @@ public class AddResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTes
URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/get").build(true)
.toUri();
String host = "www.addresponseheaderjava.org";
String expectedValue = "myresponsevalue";
String expectedValue = "myresponsevalue-www";
testClient.get().uri(uri).header("Host", host).exchange().expectHeader()
.valueEquals("example", expectedValue);
}
@@ -83,9 +83,9 @@ public class AddResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTes
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("add_response_header_java_test",
r -> r.path("/get").and().host("**.addresponseheaderjava.org")
.filters(f -> f.prefixPath("/httpbin")
.addResponseHeader("example", "myresponsevalue"))
r -> r.path("/get").and().host("{sub}.addresponseheaderjava.org")
.filters(f -> f.prefixPath("/httpbin").addResponseHeader(
"example", "myresponsevalue-{sub}"))
.uri(uri))
.build();
}

View File

@@ -55,7 +55,9 @@ public class SetRequestHeaderGatewayFilterFactoryTests extends BaseWebClientTest
.consumeWith(result -> {
Map<String, Object> headers = getMap(result.getResponseBody(),
"headers");
assertThat(headers).containsEntry("X-Req-Foo", "Second");
// add was called first, so sets will overwrite
assertThat(headers).doesNotContainEntry("X-Req-Foo", "First");
assertThat(headers).containsEntry("X-Req-Foo", "Second-www");
});
}
@@ -78,10 +80,10 @@ public class SetRequestHeaderGatewayFilterFactoryTests extends BaseWebClientTest
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("test_set_request_header",
r -> r.order(-1).host("**.setrequestheader.org")
r -> r.order(-1).host("{sub}.setrequestheader.org")
.filters(f -> f.prefixPath("/httpbin")
.addRequestHeader("X-Req-Foo", "First")
.setRequestHeader("X-Req-Foo", "Second"))
.setRequestHeader("X-Req-Foo", "Second-{sub}"))
.uri(uri))
.build();
}

View File

@@ -19,12 +19,16 @@ package org.springframework.cloud.gateway.filter.factory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory.NameValueConfig;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
@@ -41,7 +45,14 @@ public class SetResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTes
public void setResponseHeaderFilterWorks() {
testClient.get().uri("/headers").header("Host", "www.setreresponseheader.org")
.exchange().expectStatus().isOk().expectHeader()
.valueEquals("X-Request-Foo", "Bar");
.valueEquals("X-Response-Foo", "Bar");
}
@Test
public void setResponseHeaderFilterWorksJavaDsl() {
testClient.get().uri("/headers").header("Host", "www.setresponseheaderdsl.org")
.exchange().expectStatus().isOk().expectHeader()
.valueEquals("X-Res-Foo", "Second-www");
}
@Test
@@ -57,6 +68,20 @@ public class SetResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTes
@Import(DefaultTestConfig.class)
public static class TestConfig {
@Value("${test.uri}")
String uri;
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("test_set_response_header_dsl",
r -> r.order(-1).host("{sub}.setresponseheaderdsl.org")
.filters(f -> f.prefixPath("/httpbin")
.addResponseHeader("X-Res-Foo", "First")
.setResponseHeader("X-Res-Foo", "Second-{sub}"))
.uri(uri))
.build();
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2013-2019 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.support;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.expand;
public class ServerWebExchangeUtilsTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void expandWorks() {
HashMap<String, String> vars = new HashMap<>();
vars.put("foo", "bar");
vars.put("baz", "bam");
MockServerWebExchange exchange = mockExchange(vars);
String expanded = expand(exchange, "my-{foo}-{baz}");
assertThat(expanded).isEqualTo("my-bar-bam");
expanded = expand(exchange, "my-noop");
assertThat(expanded).isEqualTo("my-noop");
}
@Test
public void missingVarThrowsException() {
MockServerWebExchange exchange = mockExchange(Collections.emptyMap());
thrown.expect(IllegalArgumentException.class);
expand(exchange, "my-{foo}-{baz}");
}
private MockServerWebExchange mockExchange(Map<String, String> vars) {
MockServerHttpRequest request = MockServerHttpRequest.get("/get").build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
ServerWebExchangeUtils.putUriTemplateVariables(exchange, vars);
return exchange;
}
}

View File

@@ -303,9 +303,9 @@ spring:
- Host=**.setreresponseheader.org
- Path=/headers
filters:
- AddResponseHeader=X-Request-Foo, Bar1
- AddResponseHeader=X-Request-Foo, Bar2
- SetResponseHeader=X-Request-Foo, Bar
- AddResponseHeader=X-Response-Foo, Bar1
- AddResponseHeader=X-Response-Foo, Bar2
- SetResponseHeader=X-Response-Foo, Bar
# =====================================
- id: set_status_int_test