Skip to main content
Network Errors

How to Fix CORS 'No Access-Control-Allow-Origin' Errors

A practical guide to fixing CORS errors in the browser. Covers preflight requests, server configuration, and common mistakes.

Updated

How to Fix CORS “No Access-Control-Allow-Origin” Errors

The full error:

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

CORS (Cross-Origin Resource Sharing) is a browser security mechanism. When your JavaScript makes a request to a different origin (different protocol, domain, or port), the browser checks whether the server explicitly allows that origin. If the response doesn’t include the right headers, the browser blocks your code from reading the response. The server actually received the request and sent a response. Your code just isn’t allowed to see it.

This is a browser-only restriction. The same request from curl, Postman, or your backend works fine. That’s why it’s confusing. The issue isn’t with the request or the server’s ability to handle it. The issue is that the server didn’t tell the browser “yes, this origin is allowed to read my responses.”

How CORS actually works

Two types of requests behave differently.

Simple requests (GET, POST with simple content types, HEAD) go straight to the server. The browser sends the request, gets the response, and checks for the Access-Control-Allow-Origin header. If it’s missing or doesn’t match your origin, the response is blocked.

Preflight requests happen for anything the browser considers “not simple”: PUT, DELETE, PATCH methods, or requests with custom headers like Authorization or Content-Type: application/json. Before sending the actual request, the browser sends an OPTIONS request to the same URL. The server must respond to this OPTIONS request with the correct CORS headers. Only then does the browser send the real request.

This is where most debugging goes wrong. Developers look at the actual request in the Network tab but miss the OPTIONS preflight that failed first.

Common causes

1. Server doesn’t send CORS headers at all

The server has no CORS configuration. Every cross-origin request will fail.

Fix (Node.js/Express):

import cors from 'cors'

// Allow a specific origin
app.use(cors({
  origin: 'http://localhost:3000',
}))

Fix (bare Node.js, no library):

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')

  if (req.method === 'OPTIONS') {
    res.sendStatus(204)
    return
  }

  next()
})

Fix (Python/FastAPI):

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_methods=["*"],
    allow_headers=["*"],
)

2. Preflight OPTIONS request isn’t handled

Your server responds to GET and POST correctly, but returns a 404 or 405 for OPTIONS. The browser never gets the CORS headers from the preflight response, so it blocks the real request.

Fix: Make sure your server responds to OPTIONS on every route that receives cross-origin requests.

// Express: the cors middleware handles this automatically.
// If you're doing it manually:
app.options('*', (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  res.sendStatus(204)
})

Check the Network tab in DevTools. Filter by “Method: OPTIONS”. If the OPTIONS request has a non-2xx status or is missing the Access-Control-Allow-Origin header, that’s your problem.

3. Wildcard origin with credentials

If your fetch includes credentials: 'include' (for cookies), the server cannot respond with Access-Control-Allow-Origin: *. The browser rejects it. You must specify the exact origin.

// This combination fails:
// Client: credentials: 'include'
// Server: Access-Control-Allow-Origin: *

Fix: Return the specific origin, and add Access-Control-Allow-Credentials: true.

app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true,
}))

4. Missing Access-Control-Allow-Headers

You send a request with Content-Type: application/json or an Authorization header. The server allows the origin and the method, but doesn’t list that header in Access-Control-Allow-Headers. The preflight fails.

Fix: Include every custom header your client sends.

app.use(cors({
  origin: 'http://localhost:3000',
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
}))

5. Reverse proxy or CDN stripping CORS headers

Your backend sends the right headers, but a reverse proxy (nginx, Cloudflare, AWS ALB) sits between the browser and the server. Some proxy configurations strip or overwrite response headers.

Fix: Check the response headers in the browser’s Network tab, not on the server side. If the headers are present in server logs but absent in the browser, the proxy is stripping them.

For nginx:

location /api/ {
    proxy_pass http://backend:8080/;

    # Don't add CORS headers here if the backend already sends them
    # If the backend doesn't send them, add:
    add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;

    if ($request_method = 'OPTIONS') {
        return 204;
    }
}

The always directive is critical. Without it, nginx only adds the header for 2xx responses, and your error responses won’t have CORS headers.

The development-only shortcut

During local development, use your build tool’s proxy to avoid CORS entirely. The proxy runs on the same origin as your frontend, so CORS doesn’t apply.

// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
})

Your frontend fetches /api/users from itself (same origin, no CORS), and Vite forwards it to http://localhost:8080/api/users. This only works in development. In production, you still need server-side CORS headers or a real reverse proxy.

Do NOT do this

// Server
res.setHeader('Access-Control-Allow-Origin', '*')

Using * is fine for genuinely public APIs (like a weather service). It’s not fine for APIs that serve user-specific data, require authentication, or accept mutations. A wildcard means any site on the internet can make requests to your API from a user’s browser, using that user’s cookies if credentials are involved. Set the specific origin.

Hushbug detects CORS failures and other network errors automatically while you browse. Coming soon to the Chrome Web Store.