How to Debug CORS Errors in Local Development — Step-by-Step — HeaderSnap ModHeader was removed from Chrome & Edge over a hidden data collector — what happened and what to do
HeaderSnap
March 14, 2026

How to Debug CORS Errors in Local Development

If you’ve spent more than a few months doing web development, you’ve seen this:

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.

It shows up at the worst time — usually when you’re testing locally and everything worked fine in production (or you think it did). The error message points at the browser, but the fix is almost always on the server. The gap between those two facts is where debugging CORS gets confusing.

This guide explains what’s actually happening, how to find the specific cause quickly, and how testing with custom request headers can help you isolate problems before touching your backend config.


What CORS Actually Does

CORS (Cross-Origin Resource Sharing) is a browser security policy, not a server feature. The browser enforces it; the server just participates.

The rule is simple: if your JavaScript at http://localhost:3000 makes a request to https://api.example.com, those are two different origins (different host, protocol, or port). The browser will only allow the response through if the server explicitly says so by including Access-Control-Allow-Origin in its response headers.

Without that header, the browser blocks the response — even though the request went through, even though the server processed it and returned data. That blocked-but-already-happened quality is what makes CORS errors feel nonsensical. The request worked. The server responded. The browser just won’t show you the result.

A few terms you’ll see in error messages and network logs:

  • Simple request: A GET, HEAD, or POST with basic headers. The browser sends it directly and checks the response for CORS headers.
  • Preflight: If your request uses a non-simple method (PUT, DELETE, PATCH) or a non-simple header (like Content-Type: application/json or any custom X- header), the browser first sends an OPTIONS request. The server must respond to that preflight with CORS headers approving the actual request, or the browser stops there.
  • Access-Control-Allow-Origin: The key response header. If it matches your origin (or is *), the response goes through.
  • Access-Control-Allow-Headers: Required in the preflight response if your actual request sends custom headers.
  • Access-Control-Allow-Methods: Required if your actual request uses a non-standard method.

Why Local Development Triggers CORS More Than Production

In production, your frontend and backend typically share a domain (or you’ve already configured CORS and deployed it). In local development, you’re almost always hitting a mismatch:

  • Frontend at http://localhost:3000, backend at http://localhost:8080 — different ports, different origins
  • Frontend at http://localhost:3000, backend at https://staging-api.yourcompany.com — different host and protocol
  • Running a third-party API that has no CORS config because it was designed for server-to-server use

These combinations are common. They work in CI because CI doesn’t run in a browser. They work in Postman or curl because those tools don’t enforce CORS. They fail in the browser because that’s precisely where CORS lives.


Step One: Read the Error Message Carefully

Not all CORS errors are the same. The browser gives you useful specifics.

Blocked simple request:

No 'Access-Control-Allow-Origin' header is present on the requested resource.

The backend didn’t include the header. Either CORS isn’t configured, or it’s not configured for your origin.

Origin mismatch:

The 'Access-Control-Allow-Origin' header has a value 'https://app.example.com'
that is not equal to the supplied origin.

The server has CORS configured but for the wrong origin. If you’re testing from localhost, the server may only whitelist the production domain.

Preflight failed — missing header:

Request header field 'x-custom-auth' is not allowed by Access-Control-Allow-Headers
in preflight response.

Your request sends a custom header that the server’s preflight response doesn’t explicitly allow. The server needs Access-Control-Allow-Headers: x-custom-auth (or a wildcard) in its OPTIONS response.

Preflight failed — method not allowed:

Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.

Similar issue, but for the HTTP method.


Step Two: Check the Network Tab (Not the Console)

The console gives you the outcome. The Network tab shows you what actually happened.

Open DevTools (F12 or Cmd+Option+I), go to the Network tab, filter for Fetch/XHR, and reproduce the error. Look for:

  1. Did a preflight (OPTIONS) request fire? Find a request with method OPTIONS to the same URL. Check its response headers: does it include Access-Control-Allow-Origin, Access-Control-Allow-Headers, and Access-Control-Allow-Methods?

  2. What origin did the browser send? In the request headers, look for Origin. This is what the server’s CORS config needs to match.

  3. What did the server actually return? For simple requests, check the response headers directly. For preflights, check the OPTIONS response.

This tells you exactly what the backend needs to return to unblock you.


Step Three: Reproduce with curl

If you want to confirm what the server is actually returning (outside the browser), use curl:

# Check if a simple GET request returns CORS headers
curl -v -H "Origin: http://localhost:3000" https://api.example.com/data

# Simulate a preflight for a request with a custom header
curl -v \
  -X OPTIONS \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, X-Custom-Auth" \
  https://api.example.com/data

The curl output will show you the exact response headers the server sends. If Access-Control-Allow-Origin isn’t there, the backend isn’t sending it. If it’s there but mismatched, the origin whitelist is wrong.

This matters because curl doesn’t enforce CORS — it just shows you the raw server response. If curl shows the header is missing, the problem is server-side configuration, not browser behavior.


Step Four: Test Request Headers with a Browser Extension

Here’s where it gets useful for local development workflows: you can set the request Origin header manually in the browser to test how your server responds to different origins before changing any code.

HeaderSnap lets you add custom HTTP request headers to any URL pattern. This is useful for a few CORS debugging scenarios:

Simulate a different origin. Say your backend only allows https://app.example.com but you’re testing from localhost. You can add a rule in HeaderSnap to set Origin: https://app.example.com on requests to localhost:8080. This tells you whether the server responds correctly when it receives a whitelisted origin — without deploying code.

Header: Origin
Value: https://app.example.com
URL pattern: http://localhost:8080/*

Note: the browser still sets its own Origin in many cases, and for security reasons, not all extensions can override it for cross-origin requests. But for same-origin dev requests and API testing, this approach can confirm how your backend handles different origins.

Test which request headers trigger a preflight. If you’re unsure which header is causing the preflight, you can add them one at a time with HeaderSnap and watch the Network tab. This isolates the problem faster than reading through request code.

Environment profiles. If you switch between testing against a local backend, staging, and a third-party API, HeaderSnap’s profile feature lets you save different header configurations. Switch profiles without reconfiguring anything.

A few things to understand about HeaderSnap’s scope: it modifies both request headers (headers your browser sends) and response headers (headers your browser receives from the server). Select Request or Response when creating a rule. Note that for CORS debugging specifically, injecting request headers like Origin helps you test server behavior — but fixing a CORS error requires the server to return the correct response headers.


The Actual Fix Is Always on the Server

Once you’ve diagnosed the issue, the fix is straightforward:

Framework-specific CORS setup:

// Express (Node.js) — cors package
const cors = require('cors');
app.use(cors({
  origin: ['http://localhost:3000', 'https://app.yourapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
}));
# FastAPI (Python)
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
  CORSMiddleware,
  allow_origins=["http://localhost:3000", "https://app.yourapp.com"],
  allow_methods=["*"],
  allow_headers=["Content-Type", "Authorization"],
)
// Go (net/http)
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")

The pattern is the same everywhere:

  1. Whitelist the specific origins you want to allow (avoid * in production for authenticated endpoints)
  2. Explicitly list the headers your frontend sends in Access-Control-Allow-Headers
  3. Handle the OPTIONS preflight method

Common Mistakes

Using * with credentials. Access-Control-Allow-Origin: * does not work when the request includes credentials (credentials: 'include' in fetch). You must specify the exact origin. Also add Access-Control-Allow-Credentials: true.

Forgetting the preflight. If you fix the CORS headers for GET requests but your frontend also sends POST with Content-Type: application/json, you need to handle the OPTIONS preflight too. Check whether your framework’s CORS middleware does this automatically (most do, if configured correctly).

Fixing it in the browser instead of the server. Browser flags like --disable-web-security work for testing but should never be a permanent solution and never used in production. Similarly, browser extension workarounds that claim to “fix CORS” by injecting response headers only work for your browser — the actual deployed application still has the problem.

API proxy as a workaround. Many development setups use a local proxy (vite’s server.proxy, webpack’s devServer.proxy) to route frontend requests through the same origin. This avoids CORS entirely for local dev. It’s a valid pattern, but understand that your production backend still needs correct CORS config unless it’s always behind the same-origin proxy.


Debug Checklist

When you hit a CORS error:

  • Read the full console error — is it a missing header, an origin mismatch, or a failed preflight?
  • Check the Network tab — did a preflight fire? What did the OPTIONS response include?
  • Check the Origin header your browser sent — is this origin in your backend’s whitelist?
  • Run curl with Origin and (for preflights) Access-Control-Request-Headers to see raw server response
  • Use HeaderSnap to test request headers — simulate whitelisted origins, isolate which headers trigger preflights
  • Fix the backend config — add the origin, headers, and methods your frontend actually uses
  • Test with credentials separately if you use credentials: 'include'

CORS errors are one of those browser behaviors that feel opaque until you have the mental model — browser enforces, server permits, preflight is just the browser asking politely before committing. Once that’s clear, the debug workflow becomes mechanical: find the mismatch, fix the server config, confirm with curl.

The request header testing part is where tools like HeaderSnap earn their place in the workflow — not as a workaround, but as a way to test how your server handles different scenarios before writing any config code.

👉 Get HeaderSnap free →

Add and test request headers in your browser. No account required.


Found this helpful? Share it with your team, or check out HeaderSnap to speed up your debugging workflow.