From 5db6f63599f68a5483be1b78f830ed1ac306811c Mon Sep 17 00:00:00 2001 From: Abel Salgado Romero Date: Fri, 23 Sep 2022 19:03:13 +0200 Subject: [PATCH 1/4] Add AddRequestHeadersIfNotPresentGatewayFilterFactory This filter is similar to 'AddRequestHeaderGatewayFilterFactory' but will only add filters if there are not present, and allows setting multiple filters --- .../config/GatewayAutoConfiguration.java | 13 + ...adersIfNotPresentGatewayFilterFactory.java | 141 ++++++++++ .../route/builder/GatewayFilterSpec.java | 19 ++ .../gateway/support/KeyValueConverter.java | 24 ++ ...IfNotPresentGatewayFilterFactoryTests.java | 149 ++++++++++ .../gateway/route/builder/RouteDslTests.kt | 254 +++++++++--------- ...uest-headers-if-not-present-web-filter.yml | 17 ++ 7 files changed, 490 insertions(+), 127 deletions(-) create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/KeyValueConverter.java create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java create mode 100644 spring-cloud-gateway-server/src/test/resources/application-request-headers-if-not-present-web-filter.yml diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 68e8fe47..3b224a5c 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -68,6 +68,7 @@ import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; import org.springframework.cloud.gateway.filter.WebsocketRoutingFilter; import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter; import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.AddRequestHeadersIfNotPresentGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.CacheRequestBodyGatewayFilterFactory; @@ -143,6 +144,7 @@ import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.RouteRefreshListener; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.cloud.gateway.support.ConfigurationService; +import org.springframework.cloud.gateway.support.KeyValueConverter; import org.springframework.cloud.gateway.support.StringToZonedDateTimeConverter; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ConfigurableApplicationContext; @@ -187,6 +189,11 @@ public class GatewayAutoConfiguration { return new StringToZonedDateTimeConverter(); } + @Bean + public KeyValueConverter keyValueConverter() { + return new KeyValueConverter(); + } + @Bean public RouteLocatorBuilder routeLocatorBuilder(ConfigurableApplicationContext context) { return new RouteLocatorBuilder(context); @@ -473,6 +480,12 @@ public class GatewayAutoConfiguration { return new AddRequestHeaderGatewayFilterFactory(); } + @Bean + @ConditionalOnEnabledFilter + public AddRequestHeadersIfNotPresentGatewayFilterFactory addRequestHeadersIfNotPresentGatewayFilterFactory() { + return new AddRequestHeadersIfNotPresentGatewayFilterFactory(); + } + @Bean @ConditionalOnEnabledFilter public MapRequestHeaderGatewayFilterFactory mapRequestHeaderGatewayFilterFactory() { diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java new file mode 100644 index 00000000..4fec36b3 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java @@ -0,0 +1,141 @@ +package org.springframework.cloud.gateway.filter.factory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.core.style.ToStringCreator; +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; + +/** + * Adds one or more headers to the downstream request’s headers without overriding previous values. + * If the header is are already present, value(s) will not be set. + * + * @author Abel Salgado Romero + */ +public class AddRequestHeadersIfNotPresentGatewayFilterFactory + extends AbstractGatewayFilterFactory { + + @Override + public GatewayFilter apply(KeyValueConfig config) { + return new GatewayFilter() { + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest.Builder requestBuilder = null; + + Map> aggregatedHeaders = new HashMap<>(); + + for (KeyValue keyValue : config.getKeyValues()) { + String key = keyValue.getKey(); + List candidateValue = aggregatedHeaders.get(key); + if (candidateValue == null) { + candidateValue = new ArrayList<>(); + candidateValue.add(keyValue.getValue()); + } + else { + candidateValue.add(keyValue.getValue()); + } + aggregatedHeaders.put(key, candidateValue); + } + + for (Map.Entry> kv : aggregatedHeaders.entrySet()) { + String headerName = kv.getKey(); + + boolean headerIsMissingOrBlank = exchange.getRequest().getHeaders() + .getOrEmpty(headerName) + .stream() + .allMatch(h -> !StringUtils.hasText(h)); + + if (headerIsMissingOrBlank) { + if (requestBuilder == null) { + requestBuilder = exchange.getRequest().mutate(); + } + requestBuilder.headers(httpHeaders -> httpHeaders.addAll(headerName, kv.getValue())); + } + } + if (requestBuilder != null) { + exchange = exchange.mutate().request(requestBuilder.build()).build(); + } + return chain.filter(exchange); + } + + @Override + public String toString() { + ToStringCreator toStringCreator = filterToStringCreator(AddRequestHeadersIfNotPresentGatewayFilterFactory.this); + for (KeyValue keyValue : config.getKeyValues()) { + toStringCreator.append(keyValue.getKey(), keyValue.getValue()); + } + return toStringCreator.toString(); + } + }; + } + + public ShortcutType shortcutType() { + return ShortcutType.GATHER_LIST; + } + + @Override + public List shortcutFieldOrder() { + return Collections.singletonList("keyValues"); + } + + @Override + public KeyValueConfig newConfig() { + return new KeyValueConfig(); + } + + @Override + public Class getConfigClass() { + return KeyValueConfig.class; + } + + public static class KeyValueConfig { + + private KeyValue[] keyValues; + + public KeyValue[] getKeyValues() { + return keyValues; + } + + public void setKeyValues(KeyValue[] keyValues) { + this.keyValues = keyValues; + } + + } + + public static class KeyValue { + + private final String key; + private final String value; + + public KeyValue(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return new ToStringCreator(this).append("name", key).append("value", value).toString(); + } + + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index c49893b6..9c704a27 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.route.builder; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -37,6 +38,8 @@ import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractChangeRequestUriGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.AddRequestHeadersIfNotPresentGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.AddRequestHeadersIfNotPresentGatewayFilterFactory.KeyValue; import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.CacheRequestBodyGatewayFilterFactory; @@ -165,6 +168,22 @@ public class GatewayFilterSpec extends UriSpec { .apply(c -> c.setName(headerName).setValue(headerValue))); } + /** + * Adds a request header to the request before it is routed by the Gateway. + * @param headers the header name(s) and value(s) as 'name-1:value-1,name-2:value-2,...' + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec addRequestHeadersIfNotPresent(String... headers) { + return filter(getBean(AddRequestHeadersIfNotPresentGatewayFilterFactory.class) + .apply(c -> { + KeyValue[] values = Arrays.stream(headers) + .map(header -> header.split(":")) + .map(parts -> new KeyValue(parts[0], parts[1])) + .toArray(size -> new KeyValue[size]); + c.setKeyValues(values); + })); + } + /** * Adds a request parameter to the request before it is routed by the Gateway. * @param param the parameter name 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 new file mode 100644 index 00000000..dc122502 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/KeyValueConverter.java @@ -0,0 +1,24 @@ +package org.springframework.cloud.gateway.support; + + +import org.springframework.cloud.gateway.filter.factory.AddRequestHeadersIfNotPresentGatewayFilterFactory.KeyValue; +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; + +public class KeyValueConverter implements Converter { + + @Override + public KeyValue convert(String source) throws IllegalArgumentException { + try { + String[] split = source.split(":"); + 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'"); + } + catch (ArrayIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Invalid configuration, expected format is: 'key:value'"); + } + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java new file mode 100644 index 00000000..1a8d2084 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java @@ -0,0 +1,149 @@ +/* + * 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.filter.factory; + +import java.util.Arrays; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +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.AddRequestHeadersIfNotPresentGatewayFilterFactory.KeyValue; +import org.springframework.cloud.gateway.filter.factory.AddRequestHeadersIfNotPresentGatewayFilterFactory.KeyValueConfig; +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.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.cloud.gateway.test.TestUtils.getMap; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +@ActiveProfiles(profiles = "request-headers-if-not-present-web-filter") +public class AddRequestHeadersIfNotPresentGatewayFilterFactoryTests extends BaseWebClientTests { + + private static final String TEST_HEADER_1 = "X-Request-Example"; + private static final String TEST_HEADER_2 = "X-Request-Second-Example"; + + private static final String TEST_HOST_HEADER_VALUE = "www.addrequestheaderjava.org"; + + @Test + public void addRequestHeadersIfHeaderPresentFilterDoesNotAddHeaderIfPresent() { + final String initialHeaderValue = "initial-value"; + testClient.get().uri("/headers").header(TEST_HEADER_1, initialHeaderValue).exchange().expectBody(Map.class) + .consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), "headers"); + assertThat(headers).containsEntry(TEST_HEADER_1, initialHeaderValue); + }); + } + + @Test + public void addRequestHeadersIfNotPresentFilterWorks() { + testClient.get().uri("/headers").exchange().expectBody(Map.class) + .consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), "headers"); + assertThat(headers).containsEntry(TEST_HEADER_1, "ValueA"); + }); + } + + @Test + public void addRequestHeadersIfNotPresentFilterOnlyWorksFirstPassWhenMultipleValues() { + testClient.get().uri("/multivalueheaders").exchange() + .expectBody(Map.class).consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), "headers"); + assertThat(headers).containsEntry(TEST_HEADER_1, Arrays.asList("ValueA")); + assertThat(headers).containsEntry(TEST_HEADER_2, Arrays.asList("ValueC")); + }); + } + + @Test + public void addRequestHeadersIfNotPresentFilterWorksOnlyMissingValues() { + final String existingValue = "existing-value"; + testClient.get().uri("/multivalueheaders").header(TEST_HEADER_2, existingValue).exchange() + .expectBody(Map.class).consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), "headers"); + assertThat(headers).containsEntry(TEST_HEADER_1, Arrays.asList("ValueA")); + assertThat(headers).containsEntry(TEST_HEADER_2, Arrays.asList(existingValue)); + }); + } + + @Test + public void addRequestHeadersIfNotPresentFilterWorksJavaDsl() { + testClient.get().uri("/headers").header("Host", TEST_HOST_HEADER_VALUE).exchange().expectBody(Map.class) + .consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), "headers"); + assertThat(headers).containsEntry("X-Request-Acme", "ValueB"); + }); + } + + @Test + public void addRequestHeadersIfNotPresentFilterMultipleValuesWorksJavaDsl() { + testClient.get().uri("/multivalueheaders").header("Host", TEST_HOST_HEADER_VALUE).exchange() + .expectBody(Map.class).consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), "headers"); + assertThat(headers).containsEntry("X-Request-Acme", Arrays.asList("ValueX", "ValueY", "ValueZ")); + }); + } + + @Test + public void toStringFormat() { + KeyValueConfig keyValueConfig = new KeyValueConfig(); + keyValueConfig.setKeyValues(new KeyValue[] { + new KeyValue("my-header-name-1", "my-header-value-1"), + new KeyValue("my-header-name-2", "my-header-value-2"), + }); + GatewayFilter filter = new AddRequestHeadersIfNotPresentGatewayFilterFactory().apply(keyValueConfig); + assertThat(filter.toString()).startsWith("[AddRequestHeadersIfNotPresent") + .contains("my-header-name-1 = 'my-header-value-1'") + .contains("my-header-name-2 = 'my-header-value-2'") + .endsWith("]"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + String uri; + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("add_request_headers_if_not_present_java_test", + r -> r.path("/headers").and().host(TEST_HOST_HEADER_VALUE) + .filters(f -> f.addRequestHeadersIfNotPresent("X-Request-Acme:ValueB")) + .uri(uri)) + .route("add_multiple_request_headers_java_test", + r -> r.path("/multivalueheaders").and().host(TEST_HOST_HEADER_VALUE) + .filters(f -> f.addRequestHeadersIfNotPresent("X-Request-Acme:ValueX", "X-Request-Acme:ValueY", "X-Request-Acme:ValueZ")) + .uri(uri)) + .build(); + } + + } + +} 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 283c6322..3bdf16ff 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 diff --git a/spring-cloud-gateway-server/src/test/resources/application-request-headers-if-not-present-web-filter.yml b/spring-cloud-gateway-server/src/test/resources/application-request-headers-if-not-present-web-filter.yml new file mode 100644 index 00000000..f75cf269 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/resources/application-request-headers-if-not-present-web-filter.yml @@ -0,0 +1,17 @@ +spring: + cloud: + gateway: + routes: + - id: add_request_headers_if_not_present_test + uri: ${test.uri} + predicates: + - Path=/headers + filters: + - AddRequestHeadersIfNotPresent=X-Request-Example:ValueA + - id: add_multiple_requests_if_not_present_header_test + uri: ${test.uri} + predicates: + - Path=/multivalueheaders + filters: + - AddRequestHeadersIfNotPresent=X-Request-Example:ValueA,X-Request-Second-Example:ValueC + - AddRequestHeadersIfNotPresent=X-Request-Example:ValueB,X-Request-Second-Example:ValueD From f998ca4f754db0be1a974af93c03d88d4e9a0cbb Mon Sep 17 00:00:00 2001 From: Abel Salgado Romero Date: Tue, 27 Sep 2022 19:38:50 +0200 Subject: [PATCH 2/4] Add support for URI variables --- ...uestHeadersIfNotPresentGatewayFilterFactory.java | 10 +++++++++- ...eadersIfNotPresentGatewayFilterFactoryTests.java | 13 +++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java index 4fec36b3..d47f16c6 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java @@ -5,11 +5,13 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; 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.core.style.ToStringCreator; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.StringUtils; @@ -60,7 +62,13 @@ public class AddRequestHeadersIfNotPresentGatewayFilterFactory if (requestBuilder == null) { requestBuilder = exchange.getRequest().mutate(); } - requestBuilder.headers(httpHeaders -> httpHeaders.addAll(headerName, kv.getValue())); + ServerWebExchange finalExchange = exchange; + requestBuilder.headers(httpHeaders -> { + List replacedValues = kv.getValue().stream() + .map(value -> ServerWebExchangeUtils.expand(finalExchange, value)) + .collect(Collectors.toList()); + httpHeaders.addAll(headerName, replacedValues); + }); } } if (requestBuilder != null) { diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java index 1a8d2084..3fdaa6ed 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java @@ -95,7 +95,7 @@ public class AddRequestHeadersIfNotPresentGatewayFilterFactoryTests extends Base testClient.get().uri("/headers").header("Host", TEST_HOST_HEADER_VALUE).exchange().expectBody(Map.class) .consumeWith(result -> { Map headers = getMap(result.getResponseBody(), "headers"); - assertThat(headers).containsEntry("X-Request-Acme", "ValueB"); + assertThat(headers).containsEntry("X-Request-Acme", "ValueB-www"); }); } @@ -104,7 +104,7 @@ public class AddRequestHeadersIfNotPresentGatewayFilterFactoryTests extends Base testClient.get().uri("/multivalueheaders").header("Host", TEST_HOST_HEADER_VALUE).exchange() .expectBody(Map.class).consumeWith(result -> { Map headers = getMap(result.getResponseBody(), "headers"); - assertThat(headers).containsEntry("X-Request-Acme", Arrays.asList("ValueX", "ValueY", "ValueZ")); + assertThat(headers).containsEntry("X-Request-Acme", Arrays.asList("ValueX", "ValueY", "ValueZ", "www")); }); } @@ -134,12 +134,13 @@ public class AddRequestHeadersIfNotPresentGatewayFilterFactoryTests extends Base public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("add_request_headers_if_not_present_java_test", - r -> r.path("/headers").and().host(TEST_HOST_HEADER_VALUE) - .filters(f -> f.addRequestHeadersIfNotPresent("X-Request-Acme:ValueB")) + r -> r.path("/headers").and().host("{sub}.addrequestheaderjava.org") + .filters(f -> f.addRequestHeadersIfNotPresent("X-Request-Acme:ValueB-{sub}")) .uri(uri)) .route("add_multiple_request_headers_java_test", - r -> r.path("/multivalueheaders").and().host(TEST_HOST_HEADER_VALUE) - .filters(f -> f.addRequestHeadersIfNotPresent("X-Request-Acme:ValueX", "X-Request-Acme:ValueY", "X-Request-Acme:ValueZ")) + r -> r.path("/multivalueheaders").and().host("{sub}.addrequestheaderjava.org") + .filters(f -> f.addRequestHeadersIfNotPresent("X-Request-Acme:ValueX", "X-Request-Acme:ValueY", + "X-Request-Acme:ValueZ", "X-Request-Acme:{sub}")) .uri(uri)) .build(); } From f45d03ec1dd206f2be992e7b31342b76e49624b4 Mon Sep 17 00:00:00 2001 From: Abel Salgado Romero Date: Tue, 27 Sep 2022 19:51:18 +0200 Subject: [PATCH 3/4] Add docs --- .../main/asciidoc/spring-cloud-gateway.adoc | 47 ++++ .../gateway/support/KeyValueConverter.java | 6 +- .../gateway/route/builder/RouteDslTests.kt | 254 +++++++++--------- 3 files changed, 178 insertions(+), 129 deletions(-) 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 From 34673a9de8885c05d3fb633b04094f5334f891d9 Mon Sep 17 00:00:00 2001 From: spencergibb Date: Wed, 5 Oct 2022 11:37:40 -0400 Subject: [PATCH 4/4] Polish gh-2737 Formatting, added missing copyright and updated DisableBuiltInFiltersTests to include new filter. --- ...adersIfNotPresentGatewayFilterFactory.java | 28 ++++++++--- .../route/builder/GatewayFilterSpec.java | 16 +++--- .../gateway/support/KeyValueConverter.java | 17 ++++++- .../DisableBuiltInFiltersTests.java | 1 + ...IfNotPresentGatewayFilterFactoryTests.java | 49 +++++++++---------- 5 files changed, 68 insertions(+), 43 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java index d47f16c6..f912b928 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactory.java @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2022 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.util.ArrayList; @@ -20,8 +36,8 @@ import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; /** - * Adds one or more headers to the downstream request’s headers without overriding previous values. - * If the header is are already present, value(s) will not be set. + * Adds one or more headers to the downstream request’s headers without overriding + * previous values. If the header is are already present, value(s) will not be set. * * @author Abel Salgado Romero */ @@ -53,9 +69,7 @@ public class AddRequestHeadersIfNotPresentGatewayFilterFactory for (Map.Entry> kv : aggregatedHeaders.entrySet()) { String headerName = kv.getKey(); - boolean headerIsMissingOrBlank = exchange.getRequest().getHeaders() - .getOrEmpty(headerName) - .stream() + boolean headerIsMissingOrBlank = exchange.getRequest().getHeaders().getOrEmpty(headerName).stream() .allMatch(h -> !StringUtils.hasText(h)); if (headerIsMissingOrBlank) { @@ -79,7 +93,8 @@ public class AddRequestHeadersIfNotPresentGatewayFilterFactory @Override public String toString() { - ToStringCreator toStringCreator = filterToStringCreator(AddRequestHeadersIfNotPresentGatewayFilterFactory.this); + ToStringCreator toStringCreator = filterToStringCreator( + AddRequestHeadersIfNotPresentGatewayFilterFactory.this); for (KeyValue keyValue : config.getKeyValues()) { toStringCreator.append(keyValue.getKey(), keyValue.getValue()); } @@ -124,6 +139,7 @@ public class AddRequestHeadersIfNotPresentGatewayFilterFactory public static class KeyValue { private final String key; + private final String value; public KeyValue(String key, String value) { diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index 9c704a27..8d23d423 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -170,18 +170,16 @@ public class GatewayFilterSpec extends UriSpec { /** * Adds a request header to the request before it is routed by the Gateway. - * @param headers the header name(s) and value(s) as 'name-1:value-1,name-2:value-2,...' + * @param headers the header name(s) and value(s) as + * 'name-1:value-1,name-2:value-2,...' * @return a {@link GatewayFilterSpec} that can be used to apply additional filters */ public GatewayFilterSpec addRequestHeadersIfNotPresent(String... headers) { - return filter(getBean(AddRequestHeadersIfNotPresentGatewayFilterFactory.class) - .apply(c -> { - KeyValue[] values = Arrays.stream(headers) - .map(header -> header.split(":")) - .map(parts -> new KeyValue(parts[0], parts[1])) - .toArray(size -> new KeyValue[size]); - c.setKeyValues(values); - })); + return filter(getBean(AddRequestHeadersIfNotPresentGatewayFilterFactory.class).apply(c -> { + KeyValue[] values = Arrays.stream(headers).map(header -> header.split(":")) + .map(parts -> new KeyValue(parts[0], parts[1])).toArray(size -> new KeyValue[size]); + c.setKeyValues(values); + })); } /** 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 cf8ad848..572b4190 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 @@ -1,5 +1,20 @@ -package org.springframework.cloud.gateway.support; +/* + * Copyright 2013-2022 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 org.springframework.cloud.gateway.filter.factory.AddRequestHeadersIfNotPresentGatewayFilterFactory.KeyValue; import org.springframework.core.convert.converter.Converter; diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java index 9eea24b1..77ddb901 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java @@ -80,6 +80,7 @@ public class DisableBuiltInFiltersTests { @SpringBootTest(classes = Config.class, properties = { "spring.cloud.gateway.filter.add-request-header.enabled=false", "spring.cloud.gateway.filter.map-request-header.enabled=false", + "spring.cloud.gateway.filter.add-request-headers-if-not-present.enabled=false", "spring.cloud.gateway.filter.add-request-parameter.enabled=false", "spring.cloud.gateway.filter.add-response-header.enabled=false", "spring.cloud.gateway.filter.json-to-grpc.enabled=false", diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java index 3fdaa6ed..160aa874 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddRequestHeadersIfNotPresentGatewayFilterFactoryTests.java @@ -46,6 +46,7 @@ import static org.springframework.cloud.gateway.test.TestUtils.getMap; public class AddRequestHeadersIfNotPresentGatewayFilterFactoryTests extends BaseWebClientTests { private static final String TEST_HEADER_1 = "X-Request-Example"; + private static final String TEST_HEADER_2 = "X-Request-Second-Example"; private static final String TEST_HOST_HEADER_VALUE = "www.addrequestheaderjava.org"; @@ -62,28 +63,26 @@ public class AddRequestHeadersIfNotPresentGatewayFilterFactoryTests extends Base @Test public void addRequestHeadersIfNotPresentFilterWorks() { - testClient.get().uri("/headers").exchange().expectBody(Map.class) - .consumeWith(result -> { - Map headers = getMap(result.getResponseBody(), "headers"); - assertThat(headers).containsEntry(TEST_HEADER_1, "ValueA"); - }); + testClient.get().uri("/headers").exchange().expectBody(Map.class).consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), "headers"); + assertThat(headers).containsEntry(TEST_HEADER_1, "ValueA"); + }); } @Test public void addRequestHeadersIfNotPresentFilterOnlyWorksFirstPassWhenMultipleValues() { - testClient.get().uri("/multivalueheaders").exchange() - .expectBody(Map.class).consumeWith(result -> { - Map headers = getMap(result.getResponseBody(), "headers"); - assertThat(headers).containsEntry(TEST_HEADER_1, Arrays.asList("ValueA")); - assertThat(headers).containsEntry(TEST_HEADER_2, Arrays.asList("ValueC")); - }); + testClient.get().uri("/multivalueheaders").exchange().expectBody(Map.class).consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), "headers"); + assertThat(headers).containsEntry(TEST_HEADER_1, Arrays.asList("ValueA")); + assertThat(headers).containsEntry(TEST_HEADER_2, Arrays.asList("ValueC")); + }); } @Test public void addRequestHeadersIfNotPresentFilterWorksOnlyMissingValues() { final String existingValue = "existing-value"; - testClient.get().uri("/multivalueheaders").header(TEST_HEADER_2, existingValue).exchange() - .expectBody(Map.class).consumeWith(result -> { + testClient.get().uri("/multivalueheaders").header(TEST_HEADER_2, existingValue).exchange().expectBody(Map.class) + .consumeWith(result -> { Map headers = getMap(result.getResponseBody(), "headers"); assertThat(headers).containsEntry(TEST_HEADER_1, Arrays.asList("ValueA")); assertThat(headers).containsEntry(TEST_HEADER_2, Arrays.asList(existingValue)); @@ -104,21 +103,19 @@ public class AddRequestHeadersIfNotPresentGatewayFilterFactoryTests extends Base testClient.get().uri("/multivalueheaders").header("Host", TEST_HOST_HEADER_VALUE).exchange() .expectBody(Map.class).consumeWith(result -> { Map headers = getMap(result.getResponseBody(), "headers"); - assertThat(headers).containsEntry("X-Request-Acme", Arrays.asList("ValueX", "ValueY", "ValueZ", "www")); + assertThat(headers).containsEntry("X-Request-Acme", + Arrays.asList("ValueX", "ValueY", "ValueZ", "www")); }); } @Test public void toStringFormat() { KeyValueConfig keyValueConfig = new KeyValueConfig(); - keyValueConfig.setKeyValues(new KeyValue[] { - new KeyValue("my-header-name-1", "my-header-value-1"), - new KeyValue("my-header-name-2", "my-header-value-2"), - }); + keyValueConfig.setKeyValues(new KeyValue[] { new KeyValue("my-header-name-1", "my-header-value-1"), + new KeyValue("my-header-name-2", "my-header-value-2"), }); GatewayFilter filter = new AddRequestHeadersIfNotPresentGatewayFilterFactory().apply(keyValueConfig); assertThat(filter.toString()).startsWith("[AddRequestHeadersIfNotPresent") - .contains("my-header-name-1 = 'my-header-value-1'") - .contains("my-header-name-2 = 'my-header-value-2'") + .contains("my-header-name-1 = 'my-header-value-1'").contains("my-header-name-2 = 'my-header-value-2'") .endsWith("]"); } @@ -132,15 +129,13 @@ public class AddRequestHeadersIfNotPresentGatewayFilterFactoryTests extends Base @Bean public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { - return builder.routes() - .route("add_request_headers_if_not_present_java_test", - r -> r.path("/headers").and().host("{sub}.addrequestheaderjava.org") - .filters(f -> f.addRequestHeadersIfNotPresent("X-Request-Acme:ValueB-{sub}")) - .uri(uri)) + return builder.routes().route("add_request_headers_if_not_present_java_test", + r -> r.path("/headers").and().host("{sub}.addrequestheaderjava.org") + .filters(f -> f.addRequestHeadersIfNotPresent("X-Request-Acme:ValueB-{sub}")).uri(uri)) .route("add_multiple_request_headers_java_test", r -> r.path("/multivalueheaders").and().host("{sub}.addrequestheaderjava.org") - .filters(f -> f.addRequestHeadersIfNotPresent("X-Request-Acme:ValueX", "X-Request-Acme:ValueY", - "X-Request-Acme:ValueZ", "X-Request-Acme:{sub}")) + .filters(f -> f.addRequestHeadersIfNotPresent("X-Request-Acme:ValueX", + "X-Request-Acme:ValueY", "X-Request-Acme:ValueZ", "X-Request-Acme:{sub}")) .uri(uri)) .build(); }