Resolving Duplicate Access-Control-Allow-Origin Header Issues in Spring Cloud Gateway

This article explains why Spring Cloud Gateway may return multiple Access-Control-Allow-Origin headers, analyzes the internal processing flow that causes the duplication, and provides two practical solutions—using DedupeResponseHeader configuration or a custom GlobalFilter—to ensure a single, correct CORS header in responses.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Resolving Duplicate Access-Control-Allow-Origin Header Issues in Spring Cloud Gateway

When developing front‑end and back‑end separation with Spring Cloud, developers often encounter CORS errors, especially the "multiple Access-Control-Allow-Origin" header problem. The article first demonstrates the issue with example requests and responses, showing that both the microservice and the gateway can add the same CORS headers, resulting in duplicates that browsers reject.

Problem

In a Spring Cloud project, the front‑end accesses services directly or via Spring Cloud Gateway. Adding a global CORS filter in the service works, but the gateway also adds its own CORS headers, leading to two Access-Control-Allow-Origin and two Vary headers in the final response.

Analysis

The request first passes through DispatcherHandler, which delegates to RoutePredicateHandlerMapping. The handler then invokes DefaultCorsProcessor to apply CORS configuration from application.yml. This processor adds the Vary and Access-Control-Allow-Origin headers. Later, NettyRoutingFilter forwards the request to the downstream service and merges its response headers without deduplication, causing the duplicate headers.

Key code excerpts:

@Bean
public CorsFilter corsFilter() {
    logger.debug("CORS限制打开");
    CorsConfiguration config = new CorsConfiguration();
    // Only in dev environment set to *
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    config.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "*"
            allowedHeaders: "*"
            allowedMethods: "*"

The DefaultCorsProcessor.process method checks for existing Access-Control-Allow-Origin and skips adding another if present, but because the gateway merges headers after this step, duplication still occurs.

Solution 1: Use DedupeResponseHeader

Configure the gateway to deduplicate specific headers:

spring:
  cloud:
    gateway:
      default-filters:
        - DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials RETAIN_FIRST

The DedupeResponseHeaderGatewayFilterFactory applies strategies such as RETAIN_FIRST, RETAIN_LAST, or RETAIN_UNIQUE to remove duplicate values. For most cases, RETAIN_FIRST keeps the header set by the gateway configuration.

Solution 2: Custom GlobalFilter

Implement a filter that runs after the response is written and manually removes duplicate CORS headers:

@Component
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {
    @Override
    public int getOrder() {
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
    }
    @Override
    public Mono<Void> 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<String> value = new ArrayList<>();
                        if (kv.getValue().contains("*")) {
                            value.add("*");
                        } else {
                            value.add(kv.getValue().get(0));
                        }
                        kv.setValue(value);
                    }
                });
        }));
    }
}

When ordering filters, ensure this custom filter runs after NettyWriteResponseFilter by setting a higher order value.

Both approaches effectively eliminate duplicate CORS headers, allowing the front‑end to receive a single, valid Access-Control-Allow-Origin header and avoid browser CORS errors.

Note: The original article also includes a promotional giveaway for "实战Alibaba Sentinel" books, which is unrelated to the technical content.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaCORSSpring Cloud GatewayHeader Deduplication
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

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.