Resolving Duplicate CORS Headers in Spring Cloud Gateway Using DedupeResponseHeader and Custom GlobalFilter
This article explains why Spring Cloud Gateway can return duplicate Access-Control-Allow-Origin and Vary headers, analyzes the request processing flow, and provides two solutions—using the built‑in DedupeResponseHeader filter with retention strategies or implementing a custom CorsResponseHeaderFilter—to eliminate the CORS error caused by multiple header values.
In Spring Cloud microservice projects, CORS issues often appear when the frontend accesses services or the gateway from different domains, leading to the browser error “multiple Access‑Control‑Allow‑Origin headers”.
The article analyzes how Spring Cloud Gateway processes requests through DispatcherHandler , RoutePredicateHandlerMapping , and the DefaultCorsProcessor , showing that both the gateway configuration and the downstream service may add Vary and Access‑Control‑Allow‑Origin headers, resulting in duplicates.
Two solutions are presented. The first uses the built‑in DedupeResponseHeader filter in application.yml with strategies such as RETAIN_FIRST or RETAIN_UNIQUE to de‑duplicate the headers:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
default-filters:
- DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRSTThe second implements a custom CorsResponseHeaderFilter as a GlobalFilter that removes duplicate header values after the response is generated:
@Component
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(CorsResponseHeaderFilter.class);
private static final String ANY = "*";
@Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
}
@Override
@SuppressWarnings("serial")
public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
exchange.getResponse().getHeaders().entrySet().stream()
.filter(kv -> kv.getValue() != null && kv.getValue().size() > 1)
.filter(kv -> kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)
|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)
|| kv.getKey().equals(HttpHeaders.VARY))
.forEach(kv -> {
if (kv.getKey().equals(HttpHeaders.VARY)) {
kv.setValue(kv.getValue().stream().distinct().collect(Collectors.toList()));
} else {
List
value = new ArrayList<>();
if (kv.getValue().contains(ANY)) {
value.add(ANY);
kv.setValue(value);
} else {
value.add(kv.getValue().get(0));
kv.setValue(value);
}
}
});
}));
}
}Notes on filter ordering explain that the custom filter must have an order greater than NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER so it runs after the response body is written but before the final response is sent.
Additionally, the article warns against using Mono.defer in post‑filters because it can cause the downstream filters to execute twice; using Mono.fromRunnable avoids this problem.
Applying either the DedupeResponseHeader configuration or the custom filter ensures that only a single Access‑Control‑Allow‑Origin header is sent, eliminating the CORS error and making the gateway’s CORS handling more robust.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.