Skip to content

Commit 1f231df

Browse files
committed
Merge branch '2.1.x'
2 parents 7841bc9 + 473d437 commit 1f231df

File tree

10 files changed

+405
-3
lines changed

10 files changed

+405
-3
lines changed

docs/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<properties>
1616
<docs.main>spring-cloud-gateway</docs.main>
1717
<main.basedir>${basedir}/..</main.basedir>
18-
<docs.whitelisted.branches>1.0.x</docs.whitelisted.branches>
18+
<docs.whitelisted.branches>1.0.x,2.1.x</docs.whitelisted.branches>
1919
</properties>
2020
<build>
2121
<plugins>

docs/src/main/asciidoc/spring-cloud-gateway.adoc

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,14 +369,34 @@ spring:
369369
cloud:
370370
gateway:
371371
routes:
372-
- id: add_request_header_route
372+
- id: add_response_header_route
373373
uri: http://example.org
374374
filters:
375375
- AddResponseHeader=X-Response-Foo, Bar
376376
----
377377

378378
This will add `X-Response-Foo:Bar` header to the downstream response's headers for all matching requests.
379379

380+
=== DedupeResponseHeader GatewayFilter Factory
381+
The DedupeResponseHeader GatewayFilter Factory takes a `name` parameter and an optional `strategy` parameter. `name` can contain a list of header names, space separated.
382+
383+
.application.yml
384+
[source,yaml]
385+
----
386+
spring:
387+
cloud:
388+
gateway:
389+
routes:
390+
- id: dedupe_response_header_route
391+
uri: http://example.org
392+
filters:
393+
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
394+
----
395+
396+
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.
397+
398+
The DedupeResponseHeader filter also accepts an optional `strategy` parameter. The accepted values are `RETAIN_FIRST` (default), `RETAIN_LAST`, and `RETAIN_UNIQUE`.
399+
380400
[[hystrix]]
381401
=== Hystrix GatewayFilter Factory
382402
https://github.com/Netflix/Hystrix[Hystrix] is a library from Netflix that implements the https://martinfowler.com/bliki/CircuitBreaker.html[circuit breaker pattern].

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
5757
import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory;
5858
import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory;
59+
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
5960
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
6061
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
6162
import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory;
@@ -395,6 +396,11 @@ public ModifyRequestBodyGatewayFilterFactory modifyRequestBodyGatewayFilterFacto
395396
return new ModifyRequestBodyGatewayFilterFactory(codecConfigurer);
396397
}
397398

399+
@Bean
400+
public DedupeResponseHeaderGatewayFilterFactory dedupeResponseHeaderGatewayFilterFactory() {
401+
return new DedupeResponseHeaderGatewayFilterFactory();
402+
}
403+
398404
@Bean
399405
public ModifyResponseBodyGatewayFilterFactory modifyResponseBodyGatewayFilterFactory(
400406
ServerCodecConfigurer codecConfigurer) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright 2013-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.filter.factory;
18+
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
23+
import reactor.core.publisher.Mono;
24+
25+
import org.springframework.cloud.gateway.filter.GatewayFilter;
26+
import org.springframework.http.HttpHeaders;
27+
28+
/*
29+
Use case: Both your legacy backend and your API gateway add CORS header values. So, your consumer ends up with
30+
Access-Control-Allow-Credentials: true, true
31+
Access-Control-Allow-Origin: https://musk.mars, https://musk.mars
32+
(The one from the gateway will be the first of the two.) To fix, add
33+
DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
34+
35+
Configuration parameters:
36+
- name
37+
String representing response header names, space separated. Required.
38+
- strategy
39+
RETAIN_FIRST - Default. Retain the first value only.
40+
RETAIN_LAST - Retain the last value only.
41+
RETAIN_UNIQUE - Retain all unique values in the order of their first encounter.
42+
43+
Example 1
44+
default-filters:
45+
- DedupeResponseHeader=Access-Control-Allow-Credentials
46+
47+
Response header Access-Control-Allow-Credentials: true, false
48+
Modified response header Access-Control-Allow-Credentials: true
49+
50+
Example 2
51+
default-filters:
52+
- DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_LAST
53+
54+
Response header Access-Control-Allow-Credentials: true, false
55+
Modified response header Access-Control-Allow-Credentials: false
56+
57+
Example 3
58+
default-filters:
59+
- DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_UNIQUE
60+
61+
Response header Access-Control-Allow-Credentials: true, true
62+
Modified response header Access-Control-Allow-Credentials: true
63+
*/
64+
65+
/**
66+
* @author Vitaliy Pavlyuk
67+
*/
68+
public class DedupeResponseHeaderGatewayFilterFactory extends
69+
AbstractGatewayFilterFactory<DedupeResponseHeaderGatewayFilterFactory.Config> {
70+
71+
private static final String STRATEGY_KEY = "strategy";
72+
73+
public DedupeResponseHeaderGatewayFilterFactory() {
74+
super(Config.class);
75+
}
76+
77+
@Override
78+
public List<String> shortcutFieldOrder() {
79+
return Arrays.asList(NAME_KEY, STRATEGY_KEY);
80+
}
81+
82+
@Override
83+
public GatewayFilter apply(Config config) {
84+
return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> {
85+
dedupe(exchange.getResponse().getHeaders(), config);
86+
}));
87+
}
88+
89+
public enum Strategy {
90+
91+
/**
92+
* Default: Retain the first value only.
93+
*/
94+
RETAIN_FIRST,
95+
96+
/**
97+
* Retain the last value only.
98+
*/
99+
RETAIN_LAST,
100+
101+
/**
102+
* Retain all unique values in the order of their first encounter.
103+
*/
104+
RETAIN_UNIQUE
105+
106+
}
107+
108+
void dedupe(HttpHeaders headers, Config config) {
109+
String names = config.getName();
110+
Strategy strategy = config.getStrategy();
111+
if (headers == null || names == null || strategy == null) {
112+
return;
113+
}
114+
for (String name : names.split(" ")) {
115+
dedupe(headers, name.trim(), strategy);
116+
}
117+
}
118+
119+
private void dedupe(HttpHeaders headers, String name, Strategy strategy) {
120+
List<String> values = headers.get(name);
121+
if (values == null || values.size() <= 1) {
122+
return;
123+
}
124+
switch (strategy) {
125+
case RETAIN_FIRST:
126+
headers.set(name, values.get(0));
127+
break;
128+
case RETAIN_LAST:
129+
headers.set(name, values.get(values.size() - 1));
130+
break;
131+
case RETAIN_UNIQUE:
132+
headers.put(name, values.stream().distinct().collect(Collectors.toList()));
133+
break;
134+
default:
135+
break;
136+
}
137+
}
138+
139+
public static class Config extends AbstractGatewayFilterFactory.NameConfig {
140+
141+
private Strategy strategy = Strategy.RETAIN_FIRST;
142+
143+
public Strategy getStrategy() {
144+
return strategy;
145+
}
146+
147+
public Config setStrategy(Strategy strategy) {
148+
this.strategy = strategy;
149+
return this;
150+
}
151+
152+
}
153+
154+
}

spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public Predicate<ServerWebExchange> apply(Config config) {
8888
});
8989
}
9090
return exchange -> {
91-
PathContainer path = parsePath(exchange.getRequest().getURI().getPath());
91+
PathContainer path = parsePath(exchange.getRequest().getURI().getRawPath());
9292

9393
Optional<PathPattern> optionalPathPattern = pathPatterns.stream()
9494
.filter(pattern -> pattern.matches(path)).findFirst();

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
4040
import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory;
4141
import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory;
42+
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
43+
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.Strategy;
4244
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
4345
import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory;
4446
import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory;
@@ -177,6 +179,18 @@ public GatewayFilterSpec addResponseHeader(String headerName, String headerValue
177179
.apply(c -> c.setName(headerName).setValue(headerValue)));
178180
}
179181

182+
/**
183+
* A filter that removes duplication on a response header before it is returned to the
184+
* client by the Gateway.
185+
* @param headerName the header name(s), space separated
186+
* @param strategy RETAIN_FIRST, RETAIN_LAST, or RETAIN_UNIQUE
187+
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
188+
*/
189+
public GatewayFilterSpec dedupeResponseHeader(String headerName, String strategy) {
190+
return filter(getBean(DedupeResponseHeaderGatewayFilterFactory.class).apply(
191+
c -> c.setStrategy(Strategy.valueOf(strategy)).setName(headerName)));
192+
}
193+
180194
/**
181195
* Wraps the route in a Hystrix command. Depends on @{code
182196
* org.springframework.cloud::spring-cloud-starter-netflix-hystrix} being on the
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2013-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.filter.factory;
18+
19+
import org.junit.Test;
20+
import org.junit.runner.RunWith;
21+
22+
import org.springframework.boot.SpringBootConfiguration;
23+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
24+
import org.springframework.boot.test.context.SpringBootTest;
25+
import org.springframework.cloud.gateway.test.BaseWebClientTests;
26+
import org.springframework.context.annotation.Import;
27+
import org.springframework.test.annotation.DirtiesContext;
28+
import org.springframework.test.context.junit4.SpringRunner;
29+
30+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
31+
32+
@RunWith(SpringRunner.class)
33+
@SpringBootTest(webEnvironment = RANDOM_PORT)
34+
@DirtiesContext
35+
public class DedupeResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests {
36+
37+
@Test
38+
public void dedupeResponseHeaderFilterWorks() {
39+
testClient.get().uri("/headers").header("Host", "www.deduperesponseheader.org")
40+
.exchange().expectStatus().isOk().expectHeader()
41+
.valueEquals("Access-Control-Allow-Credentials", "true").expectHeader()
42+
.valueEquals("Access-Control-Allow-Origin", "https://musk.mars")
43+
.expectHeader().valueEquals("Scout-Cookie", "S'mores").expectHeader()
44+
.valueEquals("Next-Week-Lottery-Numbers", "4", "2", "42");
45+
}
46+
47+
@EnableAutoConfiguration
48+
@SpringBootConfiguration
49+
@Import(DefaultTestConfig.class)
50+
public static class TestConfig {
51+
52+
}
53+
54+
}

0 commit comments

Comments
 (0)