Cross-Origin Resource Sharing (CORS) is an HTTP‑header mechanism that lets a browser ask another origin for permission to read its responses. It softens the browser’s same‑origin policy so frontend code at https://app.example
can fetch from https://api.example
or any other domain when the server explicitly allows it.
- Access-Control-Allow-Origin: which origins may read the response. Use
*
for public APIs or a specific origin such ashttps://app.example
for stricter security. - Access-Control-Allow-Methods: HTTP verbs the client may use (e.g.
GET, POST, PUT, PATCH, DELETE, OPTIONS
). - Access-Control-Allow-Headers: custom request headers the browser may send, such as
Authorization
,Content-Type
, orX-CSRF-Token
. - Access-Control-Allow-Credentials: whether the browser may send cookies or HTTP‑auth headers. Must be
true
and used together with an explicit (non‑*
) origin. - Access-Control-Max-Age: how long the browser can cache the pre‑flight response, in seconds (e.g.
86400
for 24 h).
Browsers issue an OPTIONS pre‑flight request whenever a request is “non‑simple” (has custom headers, a non‑GET/POST verb, etc.). The pre‑flight must return all of the headers above or the real request is never sent.
Vercel Functions, when used standalone or through frameworks, do not add CORS headers automatically. If you are seeing CORS errors, here's how you can fix it.
const ALLOWED_ORIGIN = process.env.NODE_ENV === 'production' ? 'https://app.example' : '*';
export async function OPTIONS() { return new Response(null, { status: 200, headers: { 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Allow-Credentials': 'true', }, });}
export async function GET() { return Response.json({ ok: true }, { headers: { 'Access-Control-Allow-Origin': ALLOWED_ORIGIN }, });}
This same code example works for standalone Vercel Functions without a framework, placed at api/hello.ts
.
You can also apply headers through different configuration patterns in frameworks:
- Next.js : add a
headers()
async function innext.config.ts
that matches/api/:path*
and sets the five headers - SvelteKit : set headers in the global
handle
hook - Remix: return
json(data, { headers })
from loaders or actions - Nuxt: use
routeRules
or set headers inserver/api/*
Each method ends up setting the same headers as the examples above.
{ "headers": [ { "source": "/api/(.*)", "headers": [ { "key": "Access-Control-Allow-Origin", "value": "https://app.example" }, { "key": "Access-Control-Allow-Methods", "value": "GET, POST, OPTIONS" }, { "key": "Access-Control-Allow-Headers", "value": "Content-Type, Authorization" }, { "key": "Access-Control-Allow-Credentials", "value": "true" } ] } ]}
Placing headers in vercel.json
pushes them to Vercel’s CDN so they apply before your function runs.
If you have Deployment Protection turned on your preview or production Vercel deployments, you can use OPTIONS Allowlist to allow CORS to work on a list of paths that you define.
- When Vercel Authentication, Password Protection, or Trusted IPs is active, unauthenticated pre‑flight requests would normally be blocked.
- OPTIONS Allowlist lets you exempt specific paths from Deployment Protection only for
OPTIONS
requests./api/*
is on the allowlist by default for new projects.
Here's an example of the typical flow:
- Keep Vercel Authentication enabled for
/api/*
. - Ensure
/api
is in the OPTIONS Allowlist (default). - Browser pre‑flight succeeds; the real
POST /api/...
request still requires auth.
# Pre‑flightcurl -i -X OPTIONS https://your-domain.vercel.app/api/hello \ -H "Origin: https://app.example" \ -H "Access-Control-Request-Method: POST"
# Simple requestcurl -i https://your-domain.vercel.app/api/hello \ -H "Origin: https://app.example"
Look for a 200
status on the OPTIONS call and the correct CORS headers in both responses.
- Forgetting to add headers on error paths: wrap all return points (including 4xx/5xx) in a helper that sets CORS, or apply rules globally through configuration
- Using
*
withAccess-Control-Allow-Credentials: true
: the spec forbids this; send a specific origin instead - Excessive pre‑flight traffic: raise
Access-Control-Max-Age
(up to 86400 s = 24 h) so browsers cache the response