Go / Gin

How to Fix Gin CORS Preflight Failing With Custom Authorization Header

4 min read by DebuggedIt

Quick answer

Your frontend sends requests with an Authorization header (a bearer token, an API key) to your Gin API, and the browser blocks it with a CORS error during the...

Your frontend sends requests with an Authorization header (a bearer token, an API key) to your Gin API, and the browser blocks it with a CORS error during the preflight check, even though your CORS middleware seems to be configured. Authorization headers specifically require being listed explicitly β€” they aren't included in any default or automatically-inferred allowed header set.

The Problem

A request that includes an Authorization header fails in the browser, while the same request without that header succeeds:

fetch('https://api.example.com/data', {
  headers: {
    'Authorization': 'Bearer eyJhbGc...',
  },
})
Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy: Request header field authorization is not allowed by
Access-Control-Allow-Headers in preflight response.

Checking the actual preflight response confirms Authorization is missing from the allowed headers list:

$ curl -X OPTIONS https://api.example.com/data \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Headers: authorization" \
  -H "Access-Control-Request-Method: GET" -i
Access-Control-Allow-Headers: Content-Type

Why It Happens

Browsers require every non-"simple" header a request intends to send β€” which includes Authorization β€” to be explicitly listed in the preflight response's Access-Control-Allow-Headers. Unlike Content-Type with certain simple values, there's no automatic allowance for Authorization regardless of how permissive other parts of your CORS configuration are. This error appears when:

  • Your CORS middleware's AllowHeaders list was configured with only the headers that seemed obviously necessary at the time (commonly just Content-Type), and Authorization was overlooked entirely.
  • The middleware is correctly configured but registered after routes rather than before, so it never actually intercepts and handles the preflight OPTIONS request in time.
  • A reverse proxy or API gateway in front of Gin is stripping or overriding the Access-Control-Allow-Headers response before it reaches the browser.
  • The header casing or exact string in your allow-list configuration doesn't match what some stricter clients or proxies expect, though most modern browsers handle header name casing case-insensitively per the HTTP spec.

The Fix

Explicitly include Authorization in your CORS middleware's allowed headers list. Using gin-contrib/cors:

import "github.com/gin-contrib/cors"

router.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"https://app.example.com"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
    AllowCredentials: true,
    MaxAge:           12 * time.Hour,
}))

Make sure this middleware is registered before any route definitions, so it correctly intercepts and handles the automatic OPTIONS preflight request rather than letting it fall through to a 404 or an unhandled route:

router := gin.Default()
router.Use(cors.New(cors.Config{ /* ... */ })) // register BEFORE routes
router.GET("/data", getDataHandler)

Reload and verify the preflight response now includes Authorization:

curl -X OPTIONS https://api.example.com/data \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Headers: authorization" \
  -H "Access-Control-Request-Method: GET" -i
Access-Control-Allow-Headers: Origin,Content-Type,Authorization

If your frontend also sends cookies alongside the Authorization header, make sure AllowCredentials is true and that AllowOrigins lists specific origins rather than a wildcard β€” browsers reject the combination of a wildcard origin with credentials entirely, regardless of how the headers themselves are configured:

AllowOrigins:     []string{"https://app.example.com"}, // not "*"
AllowCredentials: true,

On the frontend side, confirm the fetch call is actually configured to send credentials if that's expected, since a missing credentials: 'include' silently drops cookies even with a fully correct server-side CORS setup:

fetch('https://api.example.com/data', {
  headers: {'Authorization': 'Bearer eyJhbGc...'},
  credentials: 'include',
})

Still Not Working?

If the header is correctly listed and the middleware is registered in the right order, but the browser still blocks the request, check whether a reverse proxy, load balancer, or API gateway sitting in front of Gin is overwriting or stripping the CORS headers Gin generates β€” test directly against the Gin service, bypassing any intermediate proxy, to isolate exactly where the header is being lost:

curl -X OPTIONS http://127.0.0.1:8080/data \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Headers: authorization" \
  -H "Access-Control-Request-Method: GET" -i

If the header appears correctly when hitting Gin directly but not through the public-facing URL, the fix belongs in your reverse proxy or gateway's own CORS-related configuration, which may need to either pass through the upstream headers unmodified or be configured with the same allowed-headers list independently.