diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index df165283..2f8c2edb 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -534,6 +534,53 @@ spring: ---- ==== +=== The `AddRequestHeadersIfNotPresent` `GatewayFilter` Factory + +The `AddRequestHeadersIfNotPresent` `GatewayFilter` factory takes a collection of `name` and `value` pairs separated by colon. +The following example configures an `AddRequestHeadersIfNotPresent` `GatewayFilter`: + +.application.yml +==== +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: add_request_headers_route + uri: https://example.org + filters: + - AddRequestHeadersIfNotPresent=X-Request-Color-1:blue,X-Request-Color-2:green +---- +==== + +This listing adds 2 headers `X-Request-Color-1:blue` and `X-Request-Color-2:green` to the downstream request's headers for all matching requests. +This is similar to how `AddRequestHeader` works, but unlike `AddRequestHeader` it will do it only if the header is not already there. +Otherwise, the original value in the client request is sent. + +Additionally, to set a multi-valued header, use the header name multiple times like `AddRequestHeadersIfNotPresent=X-Request-Color-1:blue,X-Request-Color-1:green`. + +`AddRequestHeadersIfNotPresent` also supports URI variables used to match a path or host. +URI variables may be used in the value and are expanded at runtime. +The following example configures an `AddRequestHeadersIfNotPresent` `GatewayFilter` that uses a variable: + +.application.yml +==== +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + predicates: + - Path=/red/{segment} + filters: + - AddRequestHeadersIfNotPresent=X-Request-Red:Blue-{segment} +---- +==== + === The `AddRequestParameter` `GatewayFilter` Factory The `AddRequestParameter` `GatewayFilter` Factory takes a `name` and `value` parameter. diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/KeyValueConverter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/KeyValueConverter.java index dc122502..cf8ad848 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/KeyValueConverter.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/KeyValueConverter.java @@ -7,6 +7,8 @@ import org.springframework.util.StringUtils; public class KeyValueConverter implements Converter { + private static final String INVALID_CONFIGURATION_MESSAGE = "Invalid configuration, expected format is: 'key:value'"; + @Override public KeyValue convert(String source) throws IllegalArgumentException { try { @@ -14,10 +16,10 @@ public class KeyValueConverter implements Converter { if (source.contains(":") && StringUtils.hasText(split[0])) { return new KeyValue(split[0], split.length == 1 ? "" : split[1]); } - throw new IllegalArgumentException("Invalid configuration, expected format is: 'key:value'"); + throw new IllegalArgumentException(INVALID_CONFIGURATION_MESSAGE); } catch (ArrayIndexOutOfBoundsException e) { - throw new IllegalArgumentException("Invalid configuration, expected format is: 'key:value'"); + throw new IllegalArgumentException(INVALID_CONFIGURATION_MESSAGE); } } diff --git a/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt b/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt index 3bdf16ff..283c6322 100644 --- a/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt +++ b/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt @@ -1,127 +1,127 @@ -///* -// * Copyright 2013-2020 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.route.builder -// -//import org.junit.Test -//import org.junit.runner.RunWith -//import org.springframework.beans.factory.annotation.Autowired -//import org.springframework.boot.autoconfigure.EnableAutoConfiguration -//import org.springframework.boot.test.context.SpringBootTest -//import org.springframework.cloud.gateway.support.ServerWebExchangeUtils -//import org.springframework.context.annotation.Configuration -//import org.springframework.mock.http.server.reactive.MockServerHttpRequest -//import org.springframework.mock.web.server.MockServerWebExchange -//import org.springframework.test.context.junit4.SpringRunner -//import org.springframework.web.server.ServerWebExchange -//import reactor.core.publisher.toMono -//import reactor.test.StepVerifier -//import java.net.URI -// -//@RunWith(SpringRunner::class) -//@SpringBootTest(classes = arrayOf(Config::class)) -//class RouteDslTests { -// -// @Autowired -// lateinit var builder: RouteLocatorBuilder -// -// @Test -// fun sampleRouteDsl() { -// val routeLocator = builder.routes { -// route(id = "test") { -// host("**.abc.org") and path("/image/png") -// filters { -// addResponseHeader("X-TestHeader", "foobar") -// } -// uri("http://httpbin.org:80") -// } -// -// route(id = "test2") { -// path("/image/webp") or path("/image/anotherone") -// filters { -// addResponseHeader("X-AnotherHeader", "baz") -// addResponseHeader("X-AnotherHeader-2", "baz-2") -// } -// uri("https://httpbin.org:443") -// } -// } -// -// StepVerifier -// .create(routeLocator.routes) -// .expectNextMatches({ -// it.id == "test" && it.filters.size == 1 && it.uri == URI.create("http://httpbin.org:80") -// }) -// .expectNextMatches({ -// it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") -// }) -// .expectComplete() -// .verify() -// -// val sampleExchange: ServerWebExchange = MockServerWebExchange.from(MockServerHttpRequest.get("/image/webp") -// .header("Host", "test.abc.org").build()) -// -// val filteredRoutes = routeLocator.routes.filter({ -// sampleExchange.attributes.put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, it.id) -// it.predicate.apply(sampleExchange).toMono().block() -// }) -// -// StepVerifier.create(filteredRoutes) -// .expectNextMatches({ -// it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") -// }) -// .expectComplete() -// .verify() -// } -// -// @Test -// fun dslWithFunctionParameters() { -// val routerLocator = builder.routes { -// route(id = "test1", order = 10, uri = "http://httpbin.org") { -// host("**.abc.org") -// } -// route(id = "test2", order = 10, uri = "http://someurl") { -// host("**.abc.org") -// uri("http://override-url") -// } -// } -// -// StepVerifier.create(routerLocator.routes) -// .expectNextMatches({ -// it.id == "test1" && -// it.uri == URI.create("http://httpbin.org:80") && -// it.order == 10 && -// it.predicate.apply(MockServerWebExchange -// .from(MockServerHttpRequest -// .get("/someuri").header("Host", "test.abc.org"))) -// .toMono().block() -// }) -// .expectNextMatches({ -// it.id == "test2" && -// it.uri == URI.create("http://override-url:80") && -// it.order == 10 && -// it.predicate.apply(MockServerWebExchange -// .from(MockServerHttpRequest -// .get("/someuri").header("Host", "test.abc.org"))) -// .toMono().block() -// }) -// .expectComplete() -// .verify() -// } -//} -// -//@Configuration(proxyBeanMethods = false) -//@EnableAutoConfiguration -//open class Config {} \ No newline at end of file +/* + * Copyright 2013-2020 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.route.builder + +import org.junit.Test +import org.junit.runner.RunWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.gateway.support.ServerWebExchangeUtils +import org.springframework.context.annotation.Configuration +import org.springframework.mock.http.server.reactive.MockServerHttpRequest +import org.springframework.mock.web.server.MockServerWebExchange +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.web.server.ServerWebExchange +import reactor.core.publisher.toMono +import reactor.test.StepVerifier +import java.net.URI + +@RunWith(SpringRunner::class) +@SpringBootTest(classes = arrayOf(Config::class)) +class RouteDslTests { + + @Autowired + lateinit var builder: RouteLocatorBuilder + + @Test + fun sampleRouteDsl() { + val routeLocator = builder.routes { + route(id = "test") { + host("**.abc.org") and path("/image/png") + filters { + addResponseHeader("X-TestHeader", "foobar") + } + uri("http://httpbin.org:80") + } + + route(id = "test2") { + path("/image/webp") or path("/image/anotherone") + filters { + addResponseHeader("X-AnotherHeader", "baz") + addResponseHeader("X-AnotherHeader-2", "baz-2") + } + uri("https://httpbin.org:443") + } + } + + StepVerifier + .create(routeLocator.routes) + .expectNextMatches({ + it.id == "test" && it.filters.size == 1 && it.uri == URI.create("http://httpbin.org:80") + }) + .expectNextMatches({ + it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") + }) + .expectComplete() + .verify() + + val sampleExchange: ServerWebExchange = MockServerWebExchange.from(MockServerHttpRequest.get("/image/webp") + .header("Host", "test.abc.org").build()) + + val filteredRoutes = routeLocator.routes.filter({ + sampleExchange.attributes.put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, it.id) + it.predicate.apply(sampleExchange).toMono().block() + }) + + StepVerifier.create(filteredRoutes) + .expectNextMatches({ + it.id == "test2" && it.filters.size == 2 && it.uri == URI.create("https://httpbin.org:443") + }) + .expectComplete() + .verify() + } + + @Test + fun dslWithFunctionParameters() { + val routerLocator = builder.routes { + route(id = "test1", order = 10, uri = "http://httpbin.org") { + host("**.abc.org") + } + route(id = "test2", order = 10, uri = "http://someurl") { + host("**.abc.org") + uri("http://override-url") + } + } + + StepVerifier.create(routerLocator.routes) + .expectNextMatches({ + it.id == "test1" && + it.uri == URI.create("http://httpbin.org:80") && + it.order == 10 && + it.predicate.apply(MockServerWebExchange + .from(MockServerHttpRequest + .get("/someuri").header("Host", "test.abc.org"))) + .toMono().block() + }) + .expectNextMatches({ + it.id == "test2" && + it.uri == URI.create("http://override-url:80") && + it.order == 10 && + it.predicate.apply(MockServerWebExchange + .from(MockServerHttpRequest + .get("/someuri").header("Host", "test.abc.org"))) + .toMono().block() + }) + .expectComplete() + .verify() + } +} + +@Configuration(proxyBeanMethods = false) +@EnableAutoConfiguration +open class Config {} \ No newline at end of file