Toolmingo
Guides7 min read

What Is CORS? How to Fix CORS Errors (Complete Guide)

Learn what CORS is, why browsers block cross-origin requests, and how to fix CORS errors in Node.js/Express, Python, Go, and PHP — with header reference and common pitfalls.

You've just seen it: "has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present."

This is one of the most Googled developer errors of all time — and understanding it once makes it trivial to fix every time.


What Is CORS?

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls which origins (domains) can make HTTP requests to your server.

Without CORS restrictions, any malicious website could silently make requests to your bank, email, or internal API using your logged-in session — a classic Cross-Site Request Forgery (CSRF) attack.

CORS is enforced by the browser, not the server. The server sets response headers saying "I allow these origins." The browser reads those headers and either delivers the response to your JavaScript code or blocks it.


The Same-Origin Policy

The Same-Origin Policy (SOP) is the foundation. A browser allows a page to freely make requests to the same origin it was loaded from:

Origin = scheme + host + port
https://example.com:443  →  https://example.com:443  ✅ same origin
https://example.com      →  https://api.example.com  ❌ different subdomain
https://example.com      →  http://example.com       ❌ different scheme
https://example.com      →  https://example.com:8080 ❌ different port

CORS is the mechanism that relaxes this policy in a controlled way.


Simple vs Preflighted Requests

Not all cross-origin requests are treated equally.

Simple Requests

A request is "simple" if it meets all of these:

  • Method: GET, POST, or HEAD
  • Content-Type: text/plain, multipart/form-data, or application/x-www-form-urlencoded
  • No custom headers beyond a small safe list

Simple requests go straight to the server. The browser checks the response's Access-Control-Allow-Origin header before handing the data to JavaScript.

Preflighted Requests

Anything else (JSON body, PUT/DELETE, Authorization header, etc.) triggers a preflight: the browser first sends an OPTIONS request to ask "is this allowed?" The server must respond with the right CORS headers before the actual request is made.

Browser → OPTIONS /api/data
          Origin: https://app.example.com
          Access-Control-Request-Method: POST
          Access-Control-Request-Headers: Content-Type, Authorization

Server  → 200 OK
          Access-Control-Allow-Origin: https://app.example.com
          Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
          Access-Control-Allow-Headers: Content-Type, Authorization
          Access-Control-Max-Age: 86400

Browser → POST /api/data  (actual request)

CORS Response Headers Reference

Header Purpose Example
Access-Control-Allow-Origin Which origins can read the response https://app.example.com or *
Access-Control-Allow-Methods Allowed HTTP methods (preflight) GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers Allowed request headers (preflight) Content-Type, Authorization
Access-Control-Allow-Credentials Allow cookies/auth headers true
Access-Control-Max-Age How long to cache the preflight (seconds) 86400
Access-Control-Expose-Headers Extra headers JS can read X-Total-Count, X-Request-Id

Critical rule: If Access-Control-Allow-Credentials: true, you cannot use Access-Control-Allow-Origin: * — you must specify the exact origin.


How to Fix CORS Errors

Node.js / Express

// Install: npm install cors
const express = require('express');
const cors = require('cors');
const app = express();

// Allow specific origin
app.use(cors({
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,           // allow cookies
  maxAge: 86400,               // cache preflight 24h
}));

// Dynamic origin whitelist
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
}));

// Handle preflight for all routes
app.options('*', cors());

Manual headers (without the cors package):

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

Python (FastAPI)

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],  # or ["*"] to allow all
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization"],
    max_age=86400,
)

Flask:

# Install: pip install flask-cors
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app, origins=["https://app.example.com"], supports_credentials=True)

# Per-route CORS
@app.route('/public')
@cross_origin()           # allow all origins for this route only
def public():
    return "open"

Go (net/http)

package main

import (
    "net/http"
)

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        origin := r.Header.Get("Origin")
        allowed := map[string]bool{
            "https://app.example.com":   true,
            "https://admin.example.com": true,
        }

        if allowed[origin] {
            w.Header().Set("Access-Control-Allow-Origin", origin)
            w.Header().Set("Vary", "Origin")  // important for caching
        }

        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        w.Header().Set("Access-Control-Allow-Credentials", "true")
        w.Header().Set("Access-Control-Max-Age", "86400")

        // Handle preflight
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/data", handler)
    http.ListenAndServe(":8080", corsMiddleware(mux))
}

With gorilla/mux:

// github.com/rs/cors is the standard Go CORS library
import "github.com/rs/cors"

c := cors.New(cors.Options{
    AllowedOrigins:   []string{"https://app.example.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Content-Type", "Authorization"},
    AllowCredentials: true,
    MaxAge:           86400,
})
handler := c.Handler(mux)

PHP

<?php
// Set allowed origins dynamically
$allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';

if (in_array($origin, $allowedOrigins, true)) {
    header("Access-Control-Allow-Origin: $origin");
    header("Vary: Origin");
}

header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Max-Age: 86400");

// Handle preflight — respond and stop
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

Laravel (config/cors.php):

return [
    'paths'               => ['api/*'],
    'allowed_methods'     => ['*'],
    'allowed_origins'     => ['https://app.example.com'],
    'allowed_origins_patterns' => [],
    'allowed_headers'     => ['*'],
    'exposed_headers'     => [],
    'max_age'             => 86400,
    'supports_credentials' => true,
];

Quick Reference: CORS Error → Fix

Error Message Root Cause Fix
No Access-Control-Allow-Origin header Server doesn't send CORS headers Add CORS middleware on the server
Access-Control-Allow-Origin: * with credentials Wildcard + credentials conflict Use specific origin instead of *
Preflight response status 404/405 OPTIONS route not handled Add OPTIONS handler or global preflight response
Header not in Access-Control-Allow-Headers Custom header not whitelisted Add the header name to allow_headers
Blocked by Access-Control-Allow-Methods Method not listed Add the method to allow_methods
CORS error on localhost Localhost origin mismatch (http://localhost:3000 vs http://localhost:5173) Add all dev origins to the allowed list
CORS headers missing on error responses (4xx/5xx) Middleware runs after error handler Ensure CORS middleware is first in the chain

Nginx / Apache: Reverse Proxy Fix

If your API doesn't set CORS headers, you can add them at the reverse proxy level:

Nginx:

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

    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
        add_header 'Access-Control-Max-Age' 86400;
        add_header 'Content-Length' 0;
        return 204;
    }

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

Apache (.htaccess):

Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
Header always set Access-Control-Allow-Credentials "true"

RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule .* - [R=204,L]

6 Common CORS Mistakes

  1. Using * with credentials. Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true together are invalid — browsers reject it. Always specify the exact origin when using cookies or Authorization headers.

  2. Forgetting the Vary: Origin header. When you set the Allow-Origin header dynamically (different values per request), you must also set Vary: Origin — otherwise CDNs and proxies will cache one origin's response and serve it to others.

  3. Not handling OPTIONS preflight. If your framework or router doesn't respond to OPTIONS requests, preflighted requests will fail. Add a global OPTIONS handler that returns 204 before touching auth middleware.

  4. CORS middleware after auth middleware. If authentication runs before CORS headers are set, the browser never receives the CORS headers on a 401 response and shows a CORS error instead of the real auth error. Put CORS first.

  5. Matching localhost exactly. http://localhost:3000 and http://localhost:5173 are different origins. During development, add all ports you use, or allow the entire localhost pattern.

  6. Thinking CORS is a security measure on the server. CORS only restricts browser JavaScript. A server-to-server request (curl, Postman, fetch in Node.js) ignores CORS entirely. CORS does not protect your API — authentication does.


FAQ

Why do I get a CORS error in the browser but Postman works fine?
CORS is enforced by browsers, not by HTTP. Postman, curl, and server-to-server code don't send an Origin header and aren't subject to the browser's Same-Origin Policy.

Can I disable CORS in Chrome for development?
You can launch Chrome with --disable-web-security, but this disables all origin checks — don't browse real sites in that session. Better approach: add localhost to your server's allowed origins.

What does Access-Control-Allow-Origin: * mean?
It allows any origin to read the response. Safe for fully public APIs (no cookies, no user data). Dangerous if used with an API that relies on session cookies for auth, because any site can read the response.

My CORS headers are set but I still get an error — why?
Check: (1) Is the server sending them on the OPTIONS preflight too? (2) Are they present on error responses (4xx/5xx), not just 200 responses? (3) Is there a Vary: Origin header when using dynamic origins? (4) Is the exact origin string matching (protocol + host + port)?

What's the difference between CORS and CSRF?
CORS controls which origins can read responses in the browser. CSRF is an attack where a malicious site causes your browser to send a request to another site using your cookies. CORS partially mitigates CSRF (preflighted requests require explicit allowance), but proper CSRF protection requires tokens or SameSite=Strict cookies.

How do I allow all subdomains of my domain?
There's no wildcard subdomain support in the CORS spec. You must implement it server-side: check if the Origin header matches a regex like /^https:\/\/[a-z0-9-]+\.example\.com$/ and reflect the exact origin in Access-Control-Allow-Origin. Remember to add Vary: Origin.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools