diff --git a/docs/src/main/asciidoc/ghpages.sh b/docs/src/main/asciidoc/ghpages.sh
index 57c5da3a..55e76be1 100755
--- a/docs/src/main/asciidoc/ghpages.sh
+++ b/docs/src/main/asciidoc/ghpages.sh
@@ -40,7 +40,7 @@ function check_if_anything_to_sync() {
function retrieve_current_branch() {
# Code getting the name of the current branch. For master we want to publish as we did until now
- # http://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch
+ # https://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch
# If there is a branch already passed will reuse it - otherwise will try to find it
CURRENT_BRANCH=${BRANCH}
if [[ -z "${CURRENT_BRANCH}" ]] ; then
@@ -147,7 +147,7 @@ function copy_docs_for_current_version() {
COMMIT_CHANGES="yes"
else
echo -e "Current branch is [${CURRENT_BRANCH}]"
- # http://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin
+ # https://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin
if [[ ",${WHITELISTED_BRANCHES_VALUE}," = *",${CURRENT_BRANCH},"* ]] ; then
mkdir -p ${ROOT_FOLDER}/${CURRENT_BRANCH}
echo -e "Branch [${CURRENT_BRANCH}] is whitelisted! Will copy the current docs to the [${CURRENT_BRANCH}] folder"
diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc
index a520afaa..2be60a2c 100644
--- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc
@@ -369,7 +369,7 @@ spring:
cloud:
gateway:
routes:
- - id: add_request_header_route
+ - id: add_response_header_route
uri: http://example.org
filters:
- AddResponseHeader=X-Response-Foo, Bar
@@ -377,6 +377,26 @@ spring:
This will add `X-Response-Foo:Bar` header to the downstream response's headers for all matching requests.
+=== 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.
+
+.application.yml
+[source,yaml]
+----
+spring:
+ cloud:
+ gateway:
+ routes:
+ - id: dedupe_response_header_route
+ uri: http://example.org
+ filters:
+ - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
+----
+
+This will remove duplicate values of `Access-Control-Allow-Credentials` and `Access-Control-Allow-Origin` response headers in cases when both the gateway CORS logic and the downstream add them.
+
+The DedupeResponseHeader filter also accepts an optional `strategy` parameter. The accepted values are `RETAIN_FIRST` (default), `RETAIN_LAST`, and `RETAIN_UNIQUE`.
+
[[hystrix]]
=== Hystrix GatewayFilter Factory
https://github.com/Netflix/Hystrix[Hystrix] is a library from Netflix that implements the https://martinfowler.com/bliki/CircuitBreaker.html[circuit breaker pattern].
diff --git a/mvnw b/mvnw
index a69491ac..675cb6b0 100755
--- a/mvnw
+++ b/mvnw
@@ -8,7 +8,7 @@
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
-# http://www.apache.org/licenses/LICENSE-2.0
+# 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
diff --git a/mvnw.cmd b/mvnw.cmd
index 2b934e89..4b98b78c 100644
--- a/mvnw.cmd
+++ b/mvnw.cmd
@@ -7,7 +7,7 @@
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
-@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
diff --git a/pom.xml b/pom.xml
index fd5f7c88..64610ae9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
Apache License, Version 2.0
- http://www.apache.org/licenses/LICENSE-2.0
+ https://www.apache.org/licenses/LICENSE-2.0
@@ -41,7 +41,7 @@
Spencer Gibb
sgibb at pivotal.io
Pivotal Software, Inc.
- http://www.spring.io
+ https://www.spring.io
Project lead
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
index 40e18765..e0de7e85 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
@@ -56,6 +56,7 @@ import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter;
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory;
@@ -395,6 +396,11 @@ public class GatewayAutoConfiguration {
return new ModifyRequestBodyGatewayFilterFactory(codecConfigurer);
}
+ @Bean
+ public DedupeResponseHeaderGatewayFilterFactory dedupeResponseHeaderGatewayFilterFactory() {
+ return new DedupeResponseHeaderGatewayFilterFactory();
+ }
+
@Bean
public ModifyResponseBodyGatewayFilterFactory modifyResponseBodyGatewayFilterFactory(
ServerCodecConfigurer codecConfigurer) {
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactory.java
new file mode 100644
index 00000000..f9c547ed
--- /dev/null
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactory.java
@@ -0,0 +1,149 @@
+/*
+ * 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
+ *
+ * http://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 org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.http.HttpHeaders;
+import reactor.core.publisher.Mono;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/*
+Use case: Both your legacy backend and your API gateway add CORS header values. So, your consumer ends up with
+ Access-Control-Allow-Credentials: true, true
+ Access-Control-Allow-Origin: https://musk.mars, https://musk.mars
+(The one from the gateway will be the first of the two.) To fix, add
+ DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
+
+Configuration parameters:
+- name
+ String representing response header names, space separated. Required.
+- strategy
+ RETAIN_FIRST - Default. Retain the first value only.
+ RETAIN_LAST - Retain the last value only.
+ RETAIN_UNIQUE - Retain all unique values in the order of their first encounter.
+
+Example 1
+ default-filters:
+ - DedupeResponseHeader=Access-Control-Allow-Credentials
+
+Response header Access-Control-Allow-Credentials: true, false
+Modified response header Access-Control-Allow-Credentials: true
+
+Example 2
+ default-filters:
+ - DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_LAST
+
+Response header Access-Control-Allow-Credentials: true, false
+Modified response header Access-Control-Allow-Credentials: false
+
+Example 3
+ default-filters:
+ - DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_UNIQUE
+
+Response header Access-Control-Allow-Credentials: true, true
+Modified response header Access-Control-Allow-Credentials: true
+ */
+
+/**
+ * @author Vitaliy Pavlyuk
+ */
+public class DedupeResponseHeaderGatewayFilterFactory
+ extends AbstractGatewayFilterFactory {
+
+ private static final String STRATEGY_KEY = "strategy";
+
+ public DedupeResponseHeaderGatewayFilterFactory() {
+ super(Config.class);
+ }
+
+ @Override
+ public List shortcutFieldOrder() {
+ return Arrays.asList(NAME_KEY, STRATEGY_KEY);
+ }
+
+ @Override
+ public GatewayFilter apply(Config config) {
+ return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> {
+ dedupe(exchange.getResponse().getHeaders(), config);
+ }));
+ }
+
+ public enum Strategy {
+ /*
+ Default: Retain the first value only.
+ */
+ RETAIN_FIRST,
+
+ /*
+ Retain the last value only.
+ */
+ RETAIN_LAST,
+
+ /*
+ Retain all unique values in the order of their first encounter.
+ */
+ RETAIN_UNIQUE
+ }
+
+ void dedupe(HttpHeaders headers, Config config) {
+ String names = config.getName();
+ Strategy strategy = config.getStrategy();
+ if (headers == null || names == null || strategy == null) {
+ return;
+ }
+ for (String name : names.split(" ")) {
+ dedupe(headers, name.trim(), strategy);
+ }
+ }
+
+ private void dedupe(HttpHeaders headers, String name, Strategy strategy) {
+ List values = headers.get(name);
+ if (values == null || values.size() <= 1) {
+ return;
+ }
+ switch (strategy) {
+ case RETAIN_FIRST:
+ headers.set(name, values.get(0));
+ break;
+ case RETAIN_LAST:
+ headers.set(name, values.get(values.size() - 1));
+ break;
+ case RETAIN_UNIQUE:
+ headers.put(name, values.stream().distinct().collect(Collectors.toList()));
+ break;
+ default:
+ break;
+ }
+ }
+
+ public static class Config extends AbstractGatewayFilterFactory.NameConfig {
+ private Strategy strategy = Strategy.RETAIN_FIRST;
+
+ public Strategy getStrategy() {
+ return strategy;
+ }
+
+ public Config setStrategy(Strategy strategy) {
+ this.strategy = strategy;
+ return this;
+ }
+ }
+}
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java
index 6228306d..b8ab5ec1 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java
@@ -88,7 +88,7 @@ public class PathRoutePredicateFactory
});
}
return exchange -> {
- PathContainer path = parsePath(exchange.getRequest().getURI().getPath());
+ PathContainer path = parsePath(exchange.getRequest().getURI().getRawPath());
Optional optionalPathPattern = pathPatterns.stream()
.filter(pattern -> pattern.matches(path)).findFirst();
diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java
index fa722d18..1ecfd931 100644
--- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java
+++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java
@@ -39,6 +39,8 @@ import org.springframework.cloud.gateway.filter.factory.AbstractChangeRequestUri
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.Strategy;
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory;
@@ -177,6 +179,17 @@ public class GatewayFilterSpec extends UriSpec {
.apply(c -> c.setName(headerName).setValue(headerValue)));
}
+ /**
+ * A filter that removes duplication on a response header before it is returned to the client by the Gateway.
+ * @param headerName the header name(s), space separated
+ * @param strategy RETAIN_FIRST, RETAIN_LAST, or RETAIN_UNIQUE
+ * @return a {@link GatewayFilterSpec} that can be used to apply additional filters
+ */
+ public GatewayFilterSpec dedupeResponseHeader(String headerName, String strategy) {
+ return filter(getBean(DedupeResponseHeaderGatewayFilterFactory.class)
+ .apply(c -> c.setStrategy(Strategy.valueOf(strategy)).setName(headerName)));
+ }
+
/**
* Wraps the route in a Hystrix command. Depends on @{code
* org.springframework.cloud::spring-cloud-starter-netflix-hystrix} being on the
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryTests.java
new file mode 100644
index 00000000..c96f08f8
--- /dev/null
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryTests.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013-2018 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
+ *
+ * http://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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.gateway.test.BaseWebClientTests;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = RANDOM_PORT)
+@DirtiesContext
+public class DedupeResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests {
+
+ @Test
+ public void dedupeResponseHeaderFilterWorks() {
+ testClient.get()
+ .uri("/headers")
+ .header("Host", "www.deduperesponseheader.org")
+ .exchange()
+ .expectStatus().isOk()
+ .expectHeader().valueEquals("Access-Control-Allow-Credentials", "true")
+ .expectHeader().valueEquals("Access-Control-Allow-Origin", "https://musk.mars")
+ .expectHeader().valueEquals("Scout-Cookie", "S'mores")
+ .expectHeader().valueEquals("Next-Week-Lottery-Numbers", "4", "2", "42");
+ }
+
+ @EnableAutoConfiguration
+ @SpringBootConfiguration
+ @Import(DefaultTestConfig.class)
+ public static class TestConfig { }
+
+}
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryUnitTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryUnitTests.java
new file mode 100644
index 00000000..3d2c291e
--- /dev/null
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryUnitTests.java
@@ -0,0 +1,113 @@
+/*
+ * 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
+ *
+ * http://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 org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.http.HttpHeaders;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class DedupeResponseHeaderGatewayFilterFactoryUnitTests {
+
+ private static final String NAME_1 = HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
+ private static final String NAME_2 = HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
+
+ private HttpHeaders headers;
+ private DedupeResponseHeaderGatewayFilterFactory.Config config;
+ private DedupeResponseHeaderGatewayFilterFactory filter;
+
+ @Before
+ public void setUp() {
+ headers = Mockito.mock(HttpHeaders.class);
+ config = new DedupeResponseHeaderGatewayFilterFactory.Config();
+ filter = new DedupeResponseHeaderGatewayFilterFactory();
+ }
+
+ @Test
+ public void dedupNullName() {
+ filter.dedupe(headers, config);
+ Mockito.verify(headers, Mockito.never()).get(Mockito.anyString());
+ Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), Mockito.anyString());
+ }
+
+ @Test
+ public void dedupNullValues() {
+ config.setName(NAME_1);
+ Mockito.when(headers.get(NAME_1)).thenReturn(null);
+ filter.dedupe(headers, config);
+ Mockito.verify(headers).get(NAME_1);
+ Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), Mockito.anyString());
+ }
+
+ @Test
+ public void dedupEmptyValues() {
+ config.setName(NAME_1);
+ Mockito.when(headers.get(NAME_1)).thenReturn(new ArrayList<>());
+ filter.dedupe(headers, config);
+ Mockito.verify(headers).get(NAME_1);
+ Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), Mockito.anyString());
+ }
+
+ @Test
+ public void dedupSingleValue() {
+ config.setName(NAME_1);
+ Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("1"));
+ filter.dedupe(headers, config);
+ Mockito.verify(headers).get(NAME_1);
+ Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), Mockito.anyString());
+ }
+
+
+ @Test
+ public void dedupMultipleValuesRetainFirst() {
+ config.setName(NAME_1 + " " + NAME_2);
+ Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4"));
+ Mockito.when(headers.get(NAME_2)).thenReturn(Arrays.asList("true", "false"));
+ filter.dedupe(headers, config);
+ Mockito.verify(headers).get(NAME_1);
+ Mockito.verify(headers).set(NAME_1, "2");
+ Mockito.verify(headers).get(NAME_2);
+ Mockito.verify(headers).set(NAME_2, "true");
+ Mockito.verify(headers, Mockito.times(2)).set(Mockito.anyString(), Mockito.anyString());
+ }
+
+ @Test
+ public void dedupMultipleValuesRetainLast() {
+ config.setName(NAME_1);
+ config.setStrategy(DedupeResponseHeaderGatewayFilterFactory.Strategy.RETAIN_LAST);
+ Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4"));
+ filter.dedupe(headers, config);
+ Mockito.verify(headers).get(NAME_1);
+ Mockito.verify(headers).set(NAME_1, "4");
+ Mockito.verify(headers).set(Mockito.anyString(), Mockito.anyString());
+ }
+
+ @Test
+ public void dedupMultipleValuesRetainUnique() {
+ config.setName(NAME_1);
+ config.setStrategy(DedupeResponseHeaderGatewayFilterFactory.Strategy.RETAIN_UNIQUE);
+ Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4"));
+ filter.dedupe(headers, config);
+ Mockito.verify(headers).get(NAME_1);
+ Mockito.verify(headers).put(Mockito.eq(NAME_1), Mockito.eq(Arrays.asList("2", "3", "4")));
+ Mockito.verify(headers).put(Mockito.anyString(), Mockito.anyList());
+ }
+}
\ No newline at end of file
diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java
index 4e58c53c..705dc8d3 100644
--- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java
+++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java
@@ -83,6 +83,16 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests {
expectPathRoute("/anything/multidsl3", "www.pathmultidsl.org", "path_multi_dsl");
}
+ @Test
+ public void pathRouteWorksWithPercent() {
+ testClient.get().uri("/abc/123%/function")
+ .header(HttpHeaders.HOST, "www.path.org")
+ .exchange()
+ .expectStatus().isOk()
+ .expectHeader().valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName())
+ .expectHeader().valueEquals(ROUTE_ID_HEADER, "path_test");
+ }
+
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml
index 8b293395..c362dcf0 100644
--- a/spring-cloud-gateway-core/src/test/resources/application.yml
+++ b/spring-cloud-gateway-core/src/test/resources/application.yml
@@ -69,6 +69,27 @@ spring:
- AddResponseHeader=X-Request-Foo, Bar
# =====================================
+ - id: dedupe_response_header_test
+ uri: ${test.uri}
+ predicates:
+ - Host=**.deduperesponseheader.org
+ - Path=/headers
+ filters:
+ - AddResponseHeader=Access-Control-Allow-Credentials, true
+ - AddResponseHeader=Access-Control-Allow-Credentials, false
+ - AddResponseHeader=Access-Control-Allow-Origin, https://musk.mars
+ - AddResponseHeader=Access-Control-Allow-Origin, *
+ - AddResponseHeader=Scout-Cookie, Thin Mints
+ - AddResponseHeader=Scout-Cookie, S'mores
+ - AddResponseHeader=Next-Week-Lottery-Numbers, 4
+ - AddResponseHeader=Next-Week-Lottery-Numbers, 2
+ - AddResponseHeader=Next-Week-Lottery-Numbers, 2
+ - AddResponseHeader=Next-Week-Lottery-Numbers, 42
+ - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin, RETAIN_FIRST
+ - DedupeResponseHeader=Scout-Cookie, RETAIN_LAST
+ - DedupeResponseHeader=Next-Week-Lottery-Numbers, RETAIN_UNIQUE
+
+ # =====================================
- id: rewrite_response_header_test
uri: ${test.uri}
predicates: