HTTP

Debug Guide

Debugging CORS Errors

CORS errors are browser-enforced security blocks on cross-origin requests. The fix is always server-side — you cannot solve CORS from the browser.

Step 1 — Read the Error Message

Open DevTools → Console. CORS errors look like:

Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

The key parts:

Step 2 — Identify the Request Type

CORS behaves differently for simple vs preflighted requests.

Request typeTriggerWhat to check
SimpleGET/POST with basic headersResponse has Access-Control-Allow-Origin
PreflightedCustom headers, PUT/PATCH/DELETE, JSON bodyOPTIONS response + actual response both need CORS headers

Is it a preflight? Check the Network tab. If you see an OPTIONS request before your actual request, it's preflighted.

Step 3 — Check the Server Response

In DevTools → Network → click the failing request → Response Headers. Look for:

Access-Control-Allow-Origin: https://app.example.com

Step 4 — Fix by Framework

Express.js

const cors = require('cors')

// Allow a single origin
app.use(cors({ origin: 'https://app.example.com' }))

// Allow multiple origins dynamically
const ALLOWED = new Set(['https://app.example.com', 'https://admin.example.com'])
app.use(cors({
  origin(origin, cb) {
    if (!origin || ALLOWED.has(origin)) return cb(null, true)
    cb(new Error('Not allowed by CORS'))
  },
  credentials: true
}))
    

Next.js App Router

// app/api/data/route.ts
export async function GET(request: Request) {
  return Response.json({ data: [] }, {
    headers: {
      'Access-Control-Allow-Origin': 'https://app.example.com',
      'Access-Control-Allow-Credentials': 'true'
    }
  })
}

export async function OPTIONS() {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': 'https://app.example.com',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      'Access-Control-Max-Age': '86400'
    }
  })
}
    

nginx

location /api/ {
    add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;

    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }
}

    

Step 5 — Common Mistakes

Mistake: Setting CORS headers on the wrong server. If your frontend is behind a reverse proxy (nginx, Cloudflare), the CORS headers must come from the API server, not the proxy — unless the proxy is the one handling the cross-origin request.

Mistake: Using * with credentials.

// ❌ This will fail when credentials: 'include' is set
res.set('Access-Control-Allow-Origin', '*')
res.set('Access-Control-Allow-Credentials', 'true')

// ✅ Use specific origin
res.set('Access-Control-Allow-Origin', req.headers.origin)
res.set('Vary', 'Origin')
res.set('Access-Control-Allow-Credentials', 'true')
    

Mistake: Forgetting the Vary header. When you dynamically set Access-Control-Allow-Origin based on the request origin, add Vary: Origin to prevent CDNs from caching the wrong origin's response.

Mistake: CORS on the client side. CORS is enforced by the browser. You cannot bypass it from JavaScript. The fix must be on the server. If you control neither server, use a server-side proxy.

Checklist

  • Server sends Access-Control-Allow-Origin matching your frontend origin
  • If using credentials: specific origin (not *) + Access-Control-Allow-Credentials: true
  • Preflight OPTIONS returns Access-Control-Allow-Methods and Access-Control-Allow-Headers
  • Vary: Origin is set when origin is dynamic
  • CORS headers are on the API server, not just the proxy

Frequently Asked Questions

Why does my CORS error say "No Access-Control-Allow-Origin header"?

The server is not sending the Access-Control-Allow-Origin header in its response. Add it to your server configuration. For Express.js, use the cors() middleware. For Next.js, add it in the route handler or next.config.ts headers().

Why does my preflight succeed (200) but the actual request still fails?

The OPTIONS preflight and the actual request are checked separately. A 200 preflight means the server accepted the OPTIONS request, but the actual GET/POST response may still be missing Access-Control-Allow-Origin. Check the actual request response headers, not just the preflight.

Can I use Access-Control-Allow-Origin: * with cookies?

No. Wildcard * cannot be used with credentials (cookies, Authorization header). You must specify the exact origin and also set Access-Control-Allow-Credentials: true. Using * with credentials will cause the browser to block the response.