From ca861ff4c837af19b0555fad795db8f4c888605f Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Mon, 19 May 2025 17:23:20 +0800 Subject: [PATCH 1/3] add new config for AddResponseHeaderGatewayFilterFactory Signed-off-by: jiangyuan --- ...AddResponseHeaderGatewayFilterFactory.java | 88 +++++++++++++++++-- .../route/builder/GatewayFilterSpec.java | 12 +++ ...sponseHeaderGatewayFilterFactoryTests.java | 68 ++++++++++++-- .../src/test/resources/application.yml | 1 + 4 files changed, 154 insertions(+), 15 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactory.java index 937aa196..745c5807 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactory.java @@ -16,12 +16,17 @@ package org.springframework.cloud.gateway.filter.factory; +import java.util.Arrays; +import java.util.List; + 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.HttpHeaders; +import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; @@ -29,10 +34,22 @@ import static org.springframework.cloud.gateway.support.GatewayToStringStyler.fi /** * @author Spencer Gibb */ -public class AddResponseHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory { +public class AddResponseHeaderGatewayFilterFactory + extends AbstractGatewayFilterFactory { + + private static final String OVERRIDE_KEY = "override"; + + public AddResponseHeaderGatewayFilterFactory() { + super(Config.class); + } @Override - public GatewayFilter apply(NameValueConfig config) { + public List shortcutFieldOrder() { + return Arrays.asList(GatewayFilter.NAME_KEY, GatewayFilter.VALUE_KEY, OVERRIDE_KEY); + } + + @Override + public GatewayFilter apply(Config config) { return new GatewayFilter() { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { @@ -42,19 +59,76 @@ public class AddResponseHeaderGatewayFilterFactory extends AbstractNameValueGate @Override public String toString() { return filterToStringCreator(AddResponseHeaderGatewayFilterFactory.this) - .append(config.getName(), config.getValue()) + .append(GatewayFilter.NAME_KEY, config.getName()) + .append(GatewayFilter.VALUE_KEY, config.getValue()) + .append(OVERRIDE_KEY, config.isOverride()) .toString(); } }; } - void addHeader(ServerWebExchange exchange, NameValueConfig config) { - final String value = ServerWebExchangeUtils.expand(exchange, config.getValue()); - HttpHeaders headers = exchange.getResponse().getHeaders(); + void addHeader(ServerWebExchange exchange, Config config) { // if response has been commited, no more response headers will bee added. if (!exchange.getResponse().isCommitted()) { - headers.add(config.getName(), value); + final String value = ServerWebExchangeUtils.expand(exchange, config.getValue()); + HttpHeaders headers = exchange.getResponse().getHeaders(); + if (config.override) { + headers.add(config.getName(), value); + } + else { + boolean headerIsMissingOrBlank = headers.getOrEmpty(config.getName()) + .stream() + .allMatch(h -> !StringUtils.hasText(h)); + if (headerIsMissingOrBlank) { + headers.add(config.getName(), value); + } + } } } + public static class Config { + + private String name; + + private String value; + + private boolean override = true; + + public String getName() { + return name; + } + + public Config setName(String name) { + this.name = name; + return this; + } + + public String getValue() { + return value; + } + + public Config setValue(String value) { + this.value = value; + return this; + } + + public boolean isOverride() { + return override; + } + + public Config setOverride(boolean override) { + this.override = override; + return this; + } + + @Override + public String toString() { + return new ToStringCreator(this).append(NAME_KEY, name) + .append(VALUE_KEY, value) + .append(OVERRIDE_KEY, override) + .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 47723b74..fdfec010 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 @@ -225,6 +225,18 @@ public class GatewayFilterSpec extends UriSpec { .apply(c -> c.setName(headerName).setValue(headerValue))); } + /** + * Adds a header to the response returned to the Gateway from the route. + * @param headerName the header name + * @param headerValue the header value + * @param override override or not + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec addResponseHeader(String headerName, String headerValue, boolean override) { + return filter(getBean(AddResponseHeaderGatewayFilterFactory.class) + .apply(c -> c.setName(headerName).setValue(headerValue).setOverride(override))); + } + /** * A filter that adds a local cache for storing response body for repeated requests. *

diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactoryTests.java index 4c3c002e..d42472bb 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactoryTests.java @@ -17,6 +17,8 @@ package org.springframework.cloud.gateway.filter.factory; import java.net.URI; +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -25,7 +27,6 @@ import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.gateway.filter.GatewayFilter; -import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory.NameValueConfig; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.cloud.gateway.test.BaseWebClientTests; @@ -51,20 +52,71 @@ class AddResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests { .header("Host", host) .exchange() .expectHeader() - .valueEquals("X-Request-Foo", expectedValue); + .valueEquals("X-Request-Foo", expectedValue) + .expectHeader() + .valueEquals("X-Request-Example", "ValueA"); + } + + @Test + void testResponseHeaderFilterHeaderPresent() { + URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/headers").build(true).toUri(); + String host = "www.addresponseheader.org"; + String expectedValue = "Bar"; + + Map body = new HashMap<>(); + body.put("X-Request-Example", "ValueB"); + + testClient.patch() + .uri(uri) + .header("Host", host) + .bodyValue(body) + .exchange() + .expectHeader() + .valueEquals("X-Request-Foo", expectedValue) + .expectHeader() + .valueEquals("X-Request-Example", "ValueB"); } @Test void testResponseHeaderFilterJavaDsl() { - URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/get").build(true).toUri(); + URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/headers").build(true).toUri(); String host = "www.addresponseheaderjava.org"; String expectedValue = "myresponsevalue-www"; - testClient.get().uri(uri).header("Host", host).exchange().expectHeader().valueEquals("example", expectedValue); + testClient.get() + .uri(uri) + .header("Host", host) + .exchange() + .expectHeader() + .valueEquals("example", expectedValue) + .expectHeader() + .valueEquals("example2", "myresponsevalue2-www"); + } + + @Test + void testResponseHeaderFilterHeaderPresentJavaDsl() { + URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/headers").build(true).toUri(); + String host = "www.addresponseheaderjava.org"; + String expectedValue = "myresponsevalue-www"; + + Map body = new HashMap<>(); + body.put("example2", "myresponsevalue2"); + + testClient.patch() + .uri(uri) + .header("Host", host) + .bodyValue(body) + .exchange() + .expectHeader() + .valueEquals("example", expectedValue) + .expectHeader() + .valueEquals("example2", "myresponsevalue2"); } @Test void toStringFormat() { - NameValueConfig config = new NameValueConfig().setName("myname").setValue("myvalue"); + AddResponseHeaderGatewayFilterFactory.Config config = new AddResponseHeaderGatewayFilterFactory.Config() + .setName("myname") + .setValue("myvalue"); GatewayFilter filter = new AddResponseHeaderGatewayFilterFactory().apply(config); assertThat(filter.toString()).contains("myname").contains("myvalue"); } @@ -81,11 +133,11 @@ class AddResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests { public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("add_response_header_java_test", - r -> r.path("/get") + r -> r.path("/headers") .and() .host("{sub}.addresponseheaderjava.org") - .filters( - f -> f.prefixPath("/httpbin").addResponseHeader("example", "myresponsevalue-{sub}")) + .filters(f -> f.addResponseHeader("example", "myresponsevalue-{sub}") + .addResponseHeader("example2", "myresponsevalue2-{sub}", false)) .uri(uri)) .build(); } diff --git a/spring-cloud-gateway-server/src/test/resources/application.yml b/spring-cloud-gateway-server/src/test/resources/application.yml index a08f82da..1d34803b 100644 --- a/spring-cloud-gateway-server/src/test/resources/application.yml +++ b/spring-cloud-gateway-server/src/test/resources/application.yml @@ -72,6 +72,7 @@ spring: - Path=/headers filters: - AddResponseHeader=X-Request-Foo, Bar + - AddResponseHeader=X-Request-Example, ValueA, false - id: cache_request_body_test uri: ${test.uri} From 989dd6233a61e34bf7fe3d65f06a4804dcae9e7d Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Tue, 20 May 2025 12:28:50 +0800 Subject: [PATCH 2/3] update docs Signed-off-by: jiangyuan --- .../gatewayfilter-factories/addresponseheader-factory.adoc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addresponseheader-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addresponseheader-factory.adoc index e15ea73c..f13de378 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addresponseheader-factory.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/addresponseheader-factory.adoc @@ -1,7 +1,7 @@ [[addresponseheader-gatewayfilter-factory]] = `AddResponseHeader` `GatewayFilter` Factory -The `AddResponseHeader` `GatewayFilter` Factory takes a `name` and `value` parameter. +The `AddResponseHeader` `GatewayFilter` Factory takes three parameters: `name`, `value` and `override`(default value is `true`) . The following example configures an `AddResponseHeader` `GatewayFilter`: .application.yml @@ -15,9 +15,12 @@ spring: uri: https://example.org filters: - AddResponseHeader=X-Response-Red, Blue + - AddResponseHeader=X-Response-Black, White, false ---- This adds `X-Response-Red:Blue` header to the downstream response's headers for all matching requests. +and if the response already contains the `X-Response-Black` header, this will not add the `X-Response-Black: White` +header to the downstream response's headers for all matching requests. `AddResponseHeader` is aware of URI variables used to match a path or host. URI variables may be used in the value and are expanded at runtime. From c99e29009a1ec9727b562d052fc8f6eb5d35a926 Mon Sep 17 00:00:00 2001 From: jiangyuan Date: Fri, 23 May 2025 16:03:32 +0800 Subject: [PATCH 3/3] avoid breaking changes Signed-off-by: jiangyuan --- ...AddResponseHeaderGatewayFilterFactory.java | 60 +++++++++---------- .../route/builder/GatewayFilterSpec.java | 8 ++- ...sponseHeaderGatewayFilterFactoryTests.java | 5 +- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactory.java index 745c5807..4e1d04d5 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactory.java @@ -34,13 +34,18 @@ import static org.springframework.cloud.gateway.support.GatewayToStringStyler.fi /** * @author Spencer Gibb */ -public class AddResponseHeaderGatewayFilterFactory - extends AbstractGatewayFilterFactory { +public class AddResponseHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory { private static final String OVERRIDE_KEY = "override"; - public AddResponseHeaderGatewayFilterFactory() { - super(Config.class); + @Override + public Class getConfigClass() { + return Config.class; + } + + @Override + public NameValueConfig newConfig() { + return new Config(); } @Override @@ -49,7 +54,7 @@ public class AddResponseHeaderGatewayFilterFactory } @Override - public GatewayFilter apply(Config config) { + public GatewayFilter apply(NameValueConfig config) { return new GatewayFilter() { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { @@ -58,21 +63,32 @@ public class AddResponseHeaderGatewayFilterFactory @Override public String toString() { + if (config instanceof Config) { + return filterToStringCreator(AddResponseHeaderGatewayFilterFactory.this) + .append(GatewayFilter.NAME_KEY, config.getName()) + .append(GatewayFilter.VALUE_KEY, config.getValue()) + .append(OVERRIDE_KEY, ((Config) config).isOverride()) + .toString(); + } return filterToStringCreator(AddResponseHeaderGatewayFilterFactory.this) - .append(GatewayFilter.NAME_KEY, config.getName()) - .append(GatewayFilter.VALUE_KEY, config.getValue()) - .append(OVERRIDE_KEY, config.isOverride()) + .append(config.getName(), config.getValue()) .toString(); } }; } - void addHeader(ServerWebExchange exchange, Config config) { + void addHeader(ServerWebExchange exchange, NameValueConfig config) { // if response has been commited, no more response headers will bee added. if (!exchange.getResponse().isCommitted()) { final String value = ServerWebExchangeUtils.expand(exchange, config.getValue()); HttpHeaders headers = exchange.getResponse().getHeaders(); - if (config.override) { + + boolean override = true; // default is true + if (config instanceof Config) { + override = ((Config) config).isOverride(); + } + + if (override) { headers.add(config.getName(), value); } else { @@ -86,32 +102,10 @@ public class AddResponseHeaderGatewayFilterFactory } } - public static class Config { - - private String name; - - private String value; + public static class Config extends AbstractNameValueGatewayFilterFactory.NameValueConfig { private boolean override = true; - public String getName() { - return name; - } - - public Config setName(String name) { - this.name = name; - return this; - } - - public String getValue() { - return value; - } - - public Config setValue(String value) { - this.value = value; - return this; - } - public boolean isOverride() { return override; } 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 fdfec010..5b1ed0c1 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 @@ -233,8 +233,12 @@ public class GatewayFilterSpec extends UriSpec { * @return a {@link GatewayFilterSpec} that can be used to apply additional filters */ public GatewayFilterSpec addResponseHeader(String headerName, String headerValue, boolean override) { - return filter(getBean(AddResponseHeaderGatewayFilterFactory.class) - .apply(c -> c.setName(headerName).setValue(headerValue).setOverride(override))); + AddResponseHeaderGatewayFilterFactory.Config config = new AddResponseHeaderGatewayFilterFactory.Config(); + config.setName(headerName); + config.setValue(headerValue); + config.setOverride(override); + + return filter(getBean(AddResponseHeaderGatewayFilterFactory.class).apply(config)); } /** diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactoryTests.java index d42472bb..1c58a88f 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactoryTests.java @@ -27,6 +27,7 @@ import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory.NameValueConfig; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.cloud.gateway.test.BaseWebClientTests; @@ -114,9 +115,7 @@ class AddResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests { @Test void toStringFormat() { - AddResponseHeaderGatewayFilterFactory.Config config = new AddResponseHeaderGatewayFilterFactory.Config() - .setName("myname") - .setValue("myvalue"); + NameValueConfig config = new NameValueConfig().setName("myname").setValue("myvalue"); GatewayFilter filter = new AddResponseHeaderGatewayFilterFactory().apply(config); assertThat(filter.toString()).contains("myname").contains("myvalue"); }