How to Fix Gin CORS Preflight Failing With Custom Authorization Header
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
AllowHeaderslist was configured with only the headers that seemed obviously necessary at the time (commonly justContent-Type), andAuthorizationwas overlooked entirely. - The middleware is correctly configured but registered after routes rather than before, so it never actually intercepts and handles the preflight
OPTIONSrequest in time. - A reverse proxy or API gateway in front of Gin is stripping or overriding the
Access-Control-Allow-Headersresponse 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.