API Gateway Patterns

CORS Handling at the API Gateway

How to centralize CORS configuration at the gateway instead of in each microservice — preflight caching, wildcard origins, credential handling, and security implications.

What Is CORS and Why Does It Matter?

Cross-Origin Resource Sharing (CORS) is a browser security mechanism that restricts web pages from making requests to a different domain than the one that served the page. Without CORS headers, a JavaScript application on app.example.com cannot fetch data from api.example.com — the browser blocks the response.

The browser enforces CORS; servers simply signal their policy via response headers. An API server that wants to allow cross-origin requests must respond with Access-Control-Allow-Origin and related headers.

CORS is purely a browser enforcement mechanism — it does not protect APIs from server-to-server calls, curl, or native apps. It only controls browser behavior.

Why Centralize CORS at the Gateway?

When each microservice handles its own CORS configuration:

  • One service might return Access-Control-Allow-Origin: * while another requires specific origins — inconsistency causes mysterious browser errors
  • Preflight (OPTIONS) requests reach backend services unnecessarily, consuming resources for what is essentially a gateway-level concern
  • CORS policy changes (adding a new allowed origin) require updating every service

Handling CORS at the gateway solves all three problems: consistent policy, preflights answered without hitting backends, and a single configuration point.

How Preflight Works

For requests with custom headers or non-simple methods, browsers send an OPTIONS "preflight" request before the actual request to check if the server allows it:

# Browser sends preflight:
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

# Gateway responds:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Vary: Origin

The gateway intercepts OPTIONS requests, responds immediately without contacting the backend, and the browser proceeds with the actual request.

Access-Control-Max-Age: Preflight Caching

Access-Control-Max-Age tells the browser how long to cache the preflight response in seconds. Set this to a large value (86400 = 24 hours) to minimize preflight overhead — browsers will not re-send OPTIONS for the same request pattern within the cache window. Note: Chrome caps this at 7200 seconds regardless of the value.

Origin Configuration

Specific Origin Whitelist

The most secure approach: only allow explicitly listed origins:

# Kong CORS plugin — specific origins
plugins:
  - name: cors
    config:
      origins:
        - https://app.example.com
        - https://admin.example.com
        - https://partner.other.com
      methods: [GET, POST, PUT, DELETE, OPTIONS]
      headers: [Content-Type, Authorization, X-Request-ID]
      max_age: 86400
      credentials: true

The gateway compares the Origin request header against the whitelist and reflects the matching origin in Access-Control-Allow-Origin. This is important: you cannot return multiple origins in Access-Control-Allow-Origin — you must reflect the specific requesting origin. Always include Vary: Origin to prevent caches from serving one client's CORS headers to a different-origin client:

Vary: Origin
Access-Control-Allow-Origin: https://app.example.com  ← reflected, not wildcard

Wildcard Origin

Access-Control-Allow-Origin: *

Wildcard allows any origin. Use it only for truly public, read-only APIs — public datasets, open APIs, CDN-served assets. Wildcard cannot be combined with Access-Control-Allow-Credentials: true. Browsers will reject the response:

# This combination is invalid and will be blocked by browsers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true  ← INVALID

Dynamic Origin Reflection (and Its Risks)

Some gateways reflect the Origin header back unconditionally — regardless of whether it is in a whitelist. This is dangerous because it effectively allows any origin, even while appearing to restrict access:

# DANGEROUS — do not do this:
# Gateway reflects any Origin it receives, no validation
Access-Control-Allow-Origin: https://evil.com  ← reflected without checking
Access-Control-Allow-Credentials: true

A malicious site can exploit this to make credentialed cross-origin requests to your API. Always validate the Origin against a whitelist before reflecting.

Regex Matching for Subdomains

Allow all subdomains of a trusted domain:

# Envoy CORS filter with regex
http_filters:
  - name: envoy.filters.http.cors
    typed_config:
      allow_origin_string_match:
        - safe_regex:
            regex: "https://[a-z0-9-]+\.example\.com"

Credential Handling

Credentials (cookies, authorization headers, TLS client certificates) are not sent in cross-origin requests by default. To include them, the client must opt in:

// Browser: opt in to sending credentials
fetch('https://api.example.com/users', {
    credentials: 'include',  // send cookies and Authorization header
    headers: { Authorization: 'Bearer ...' }
})

And the server must explicitly allow it:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://app.example.com  ← must be specific, not *

Cookies with SameSite=Strict or SameSite=Lax will not be sent on cross-origin requests even if CORS allows them. Cookies that must travel cross-origin require SameSite=None; Secure. Ensure gateway CORS configuration aligns with cookie SameSite settings, otherwise credentialed requests will fail silently.

Gateway Configuration Reference

# AWS API Gateway CORS (OpenAPI 3 extension)
x-amazon-apigateway-cors:
  allowOrigins:
    - "https://app.example.com"
    - "https://admin.example.com"
  allowMethods:
    - GET
    - POST
    - PUT
    - DELETE
    - OPTIONS
  allowHeaders:
    - Content-Type
    - Authorization
    - X-Amz-Date
  exposeHeaders:
    - X-Request-ID
    - RateLimit-Remaining
  maxAge: 86400
  allowCredentials: true

Summary

Handle CORS at the gateway to ensure consistent policy across all services and to answer preflight requests without involving backends. Use a specific origin whitelist rather than wildcard for any API that uses credentials. Always include Vary: Origin to prevent cache poisoning. Never reflect Origin without whitelist validation — unconditional reflection is equivalent to Allow-Origin: * with the appearance of restriction. Set Access-Control-Max-Age to 86400 to minimize preflight overhead.

Related Protocols

Related Glossary Terms

More in API Gateway Patterns