Skip to content

add AddResponseHeadersIfNotPresentGatewayFilterFactory #3756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestheadersifnotpresent-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/addrequestparameter-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/addresponseheader-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/addresponseheadersifnotpresent-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/circuitbreaker-filter-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/cacherequestbody-factory.adoc[]
*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/deduperesponseheader-factory.adoc[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[[addresponseheadersifnotpresent-gatewayfilter-factory]]
= `AddResponseHeadersIfNotPresent` `GatewayFilter` Factory

The `AddResponseHeadersIfNotPresent` `GatewayFilter` factory takes a collection of `name` and `value` pairs separated by colon.
The following example configures an `AddResponseHeadersIfNotPresent` `GatewayFilter`:

.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: add_response_headers_if_not_present_route
uri: https://example.org
filters:
- AddResponseHeadersIfNotPresent=X-Response-Color-1:blue,X-Response-Color-2:green
----


This listing adds 2 headers `X-Response-Color-1:blue` and `X-Response-Color-2:green` to the downstream response's headers for all matching requests.
This is similar to how `AddResponseHeader` works, but unlike `AddResponseHeader` it will do it only if the header is not already there.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be useful to also add the option to override the header if it is present? The default could be to do nothing if the header is there but we could add a configuration option to allow the override behavior

cc: @spencergibb

Otherwise, the original value in the response is return.

Additionally, to set a multi-valued header, use the header name multiple times like `AddResponseHeadersIfNotPresent=X-Response-Color-1:blue,X-Response-Color-1:green`.

`AddResponseHeadersIfNotPresent` 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 `AddResponseHeadersIfNotPresent` `GatewayFilter` that uses a variable:

.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: add_response_headers_if_not_present_route
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
- AddResponseHeadersIfNotPresent=X-Response-Red:Blue-{segment}
----
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
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.AddResponseHeadersIfNotPresentGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.CacheRequestBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
Expand Down Expand Up @@ -551,6 +552,12 @@ public AddResponseHeaderGatewayFilterFactory addResponseHeaderGatewayFilterFacto
return new AddResponseHeaderGatewayFilterFactory();
}

@Bean
@ConditionalOnEnabledFilter
public AddResponseHeadersIfNotPresentGatewayFilterFactory addResponseHeadersIfNotPresentGatewayFilterFactory() {
return new AddResponseHeadersIfNotPresentGatewayFilterFactory();
}

@Bean
@ConditionalOnEnabledFilter
public ModifyRequestBodyGatewayFilterFactory modifyRequestBodyGatewayFilterFactory(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2013-2025 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;
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.cloud.gateway.support.config.KeyValue;
import org.springframework.cloud.gateway.support.config.KeyValueConfig;
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;

/**
* Adds one or more headers to the response’s headers without overriding previous values.
* If the header is are already present, value(s) will not be set.
*
* @author jiangyuan
*/
public class AddResponseHeadersIfNotPresentGatewayFilterFactory extends AbstractGatewayFilterFactory<KeyValueConfig> {

@Override
public GatewayFilter apply(KeyValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> addResponseHeaderIfNotPresent(exchange, config)));
}

@Override
public String toString() {
ToStringCreator toStringCreator = filterToStringCreator(
AddResponseHeadersIfNotPresentGatewayFilterFactory.this);
for (KeyValue keyValue : config.getKeyValues()) {
toStringCreator.append(keyValue.getKey(), keyValue.getValue());
}
return toStringCreator.toString();
}
};
}

private void addResponseHeaderIfNotPresent(ServerWebExchange exchange, KeyValueConfig config) {
if (!exchange.getResponse().isCommitted()) {
Map<String, List<String>> aggregatedHeaders = new HashMap<>();
for (KeyValue keyValue : config.getKeyValues()) {
String key = keyValue.getKey();
List<String> candidateValue = aggregatedHeaders.get(key);
if (candidateValue == null) {
candidateValue = new ArrayList<>();
candidateValue.add(keyValue.getValue());
aggregatedHeaders.put(key, candidateValue);
}
else {
candidateValue.add(keyValue.getValue());
}
}

HttpHeaders headers = exchange.getResponse().getHeaders();
for (Map.Entry<String, List<String>> kv : aggregatedHeaders.entrySet()) {
String headerName = kv.getKey();

boolean headerIsMissingOrBlank = headers.getOrEmpty(headerName)
.stream()
.allMatch(h -> !StringUtils.hasText(h));

if (headerIsMissingOrBlank) {
List<String> replacedValues = kv.getValue()
.stream()
.map(value -> ServerWebExchangeUtils.expand(exchange, value))
.collect(Collectors.toList());
headers.addAll(headerName, replacedValues);
}
}
}
}

@Override
public ShortcutType shortcutType() {
return ShortcutType.GATHER_LIST;
}

@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("keyValues");
}

@Override
public KeyValueConfig newConfig() {
return new KeyValueConfig();
}

@Override
public Class<KeyValueConfig> getConfigClass() {
return KeyValueConfig.class;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
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.AddResponseHeadersIfNotPresentGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.CacheRequestBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.Strategy;
Expand Down Expand Up @@ -225,6 +226,22 @@ public GatewayFilterSpec addResponseHeader(String headerName, String headerValue
.apply(c -> c.setName(headerName).setValue(headerValue)));
}

/**
* Adds a header to the response returned to the Gateway from the route.
* @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 addResponseHeadersIfNotPresent(String... headers) {
return filter(getBean(AddResponseHeadersIfNotPresentGatewayFilterFactory.class).apply(c -> {
org.springframework.cloud.gateway.support.config.KeyValue[] values = Arrays.stream(headers)
.map(header -> header.split(":"))
.map(parts -> new org.springframework.cloud.gateway.support.config.KeyValue(parts[0], parts[1]))
.toArray(size -> new org.springframework.cloud.gateway.support.config.KeyValue[size]);
c.setKeyValues(values);
}));
}

/**
* A filter that adds a local cache for storing response body for repeated requests.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public void shouldInjectOnlyEnabledBuiltInFilters() {
"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.add-response-headers-if-not-present.enabled=false",
"spring.cloud.gateway.filter.json-to-grpc.enabled=false",
"spring.cloud.gateway.filter.modify-request-body.enabled=false",
"spring.cloud.gateway.filter.local-response-cache.enabled=false",
Expand Down
Loading
Loading