CORS preflight failing: "Request header field … is not allowed by Access-Control-Allow-Headers"
Filed under APIs, auth & CORS
Your request carries a header that triggers a browser preflight, and the server either never answers the OPTIONS rehearsal or answers without listing that header as allowed.
Fastest route
Give your AI this prompt
Copy it, replace the bracketed part with your error, and send it.
Preview the prompt
My browser console says: "Request header field [header name] is not allowed by Access-Control-Allow-Headers in preflight response" (or the preflight itself fails). My setup: the frontend at [your origin] calls [the API URL]. The API is [my Express server / my Next.js route handler / a Supabase Edge Function]. From DevTools Network, the OPTIONS request returns status [paste it] with response headers: [paste them]. The headers my request sends are: [list them, including any a library adds, like Authorization or apikey]. Please: 1) tell me whether the server is failing to answer OPTIONS at all, or answering without allowing that header, 2) show the exact server-side change for my stack so the preflight returns 200/204 with Access-Control-Allow-Origin for my origin and Access-Control-Allow-Headers including the named header, 3) make sure the same headers are also set on the real response. Constraints: do not fix this by removing the header from my request, changing my Content-Type, or using mode: "no-cors"; do not suggest a browser extension; change the server side only, one step at a time.
Fix it yourself
Follow the repair
- Read the error literally, then inspect the OPTIONS request. The browser's message names the exact header being rejected: "Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response." That is your shopping list. Then open DevTools, go to Network, trigger the failure, and click the OPTIONS request. Its status splits the problem in two: 404, 405, or 500 means the server is not handling preflights at all; a 200 or 204 means it answers, but its Access-Control-Allow-Headers value does not include the named header. The fixes differ, so know which one you have.
- On Express, let the cors middleware answer preflights — globally, before your routes. The cors package handles OPTIONS automatically, but only for requests that pass through it, so it must be registered with app.use before your route definitions, not attached to individual routes the preflight never reaches.
List every custom header your frontend sends in allowedHeaders (or leave the option out and the package reflects what the browser asks for). If you wrote your own header-setting middleware instead, you must answer OPTIONS explicitly with a 204 and the same headers — a handler that only decorates GET and POST responses leaves the preflight to die on its own.// npm install cors import cors from "cors"; import express from "express"; const app = express(); app.use(cors({ origin: "https://your-site.com", allowedHeaders: ["Content-Type", "Authorization"], })); - In a Next.js API route, export an OPTIONS handler that allows the headers. Route handlers do not answer preflight automatically. Add an OPTIONS export alongside your GET or POST, returning the permission headers — including the one your error named.
If you handle CORS in a middleware.ts instead, check its matcher actually covers the API path being called — a matcher that skips the route means the preflight meets nothing at all.const corsHeaders = { "Access-Control-Allow-Origin": "https://your-site.com", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", }; export async function OPTIONS() { return new Response(null, { status: 204, headers: corsHeaders }); } export async function POST() { // your handler, returning responses with corsHeaders attached return Response.json({ ok: true }, { headers: corsHeaders }); } - On a Supabase Edge Function, handle OPTIONS first and list Supabase's own headers. Edge Functions receive the preflight like any other request, and if your function ignores it, every call from the browser fails — because supabase-js always sends apikey and Authorization headers, so every call is preflighted. Supabase's documented pattern is a shared cors.ts:
And in the function itself, answer the preflight before doing anything else:// supabase/functions/_shared/cors.ts export const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", };import { corsHeaders } from "../_shared/cors.ts"; Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response("ok", { headers: corsHeaders }); } // real work here — and include corsHeaders on this response too }); - Verify against the preflight, not the error message. Reload, trigger the call again, and click the OPTIONS request in the Network tab. You want: status 200 or 204, an Access-Control-Allow-Origin matching your site's origin, and an Access-Control-Allow-Headers value that contains the header the error named. The moment those three line up, the real request fires immediately behind the preflight and you will see both in the list. If the preflight succeeds but the real request now fails with a different CORS message about the origin header, that is the sibling problem — the response to the actual request needs the same origin header attached.
- Do not dodge the preflight by stripping your request down. It is tempting to remove the Authorization header or send the body as text/plain so the request counts as "simple" and skips the rehearsal. That can silence the error while breaking authentication or leaving your server parsing a body it did not expect — the error gone, the feature quietly wrong. Preflight is not the enemy; it is a checklist the server is failing. Answer the checklist on the server and every browser, not just yours, gets through.
Why this happensOptional explanation
Some requests are "simple" — a plain GET, or a form-style post — and browsers send those straight away. The moment your request carries something custom, the rules change: an Authorization header, any X- custom header, or a Content-Type of application/json all make the request "non-simple," and before sending it the browser asks permission with a preflight. The preflight is an OPTIONS request to the same address, carrying two telling headers: Origin, and Access-Control-Request-Headers, which lists the headers the real request wants to use.
The server is supposed to answer: yes, that origin is allowed, and yes, those headers are allowed — in the form of Access-Control-Allow-Origin and Access-Control-Allow-Headers response headers. This error appears when the answer fails the second half of that check: the named header is not on the server's list, or the preflight never got a usable answer because the server has no idea what to do with an OPTIONS request. It is a close cousin of the missing Access-Control-Allow-Origin error, but one step earlier in the conversation — the real request was never even attempted, which is why fixing your fetch code changes nothing.
- The header is not on the allowed list: the server's Access-Control-Allow-Headers does not include the one the error names.
- OPTIONS goes unanswered: the server or framework returns 404 or 405 for preflight requests because nothing handles them.
- Middleware in the wrong place: CORS is set on individual routes, but the preflight arrives before routing — or after the middleware that would answer it.
- A helper adds a header you forgot: libraries like Supabase's client quietly attach apikey and Authorization headers, so the preflight asks for headers you never typed.
Quick answers
Why does this only happen on some of my requests?
Simple requests — a plain GET, a basic form post — go straight through. Adding an Authorization header, a custom header, or a JSON Content-Type makes the request non-simple, so the browser first sends an OPTIONS preflight to ask whether those headers may be used. That is why adding a login token or switching to JSON can break a call that worked yesterday.
How do I know which headers I need to allow?
The error names the first header the server refused. Open the OPTIONS request in the Network tab and compare its Access-Control-Allow-Headers response value with what your request sends — remembering that libraries add their own: supabase-js attaches apikey and Authorization, for example. Every requested header must appear on the server's allowed list.
Can't I avoid preflight by sending the body as plain text instead of JSON?
It can silence the error, at the price of your server receiving a body in a format it may not parse and your code losing the headers it needs — the failure just moves somewhere quieter. Answering the preflight properly on the server is a few lines, works for every visitor's browser, and keeps your request exactly as designed.