Backend Development 10 min read

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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Resolving Duplicate CORS Headers in Spring Cloud Gateway Using DedupeResponseHeader and Custom GlobalFilter

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_FIRST

The 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.

backendJavamicroservicesSpring BootCORSSpring Cloud Gateway
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.