Back to Blog
March 1, 2026 Part 2

CORS error: Access-Control-Allow-Origin header missing

This is the CORS error you'll see more than any other. It's often the first one developers ever encounter, and it's also one of the most misdiagnosed. That's because the same error message can have four or five completely different root causes.

I
I-Hate-CORS team
Author
CORS error: Access-Control-Allow-Origin header missing

This is the second post in our CORS error series. If you haven’t read the introduction on systematic CORS debugging, start there. It’ll make everything in this post easier to follow.


This is the CORS error you’ll see more than any other. It’s often the first one developers ever encounter, and it’s also one of the most misdiagnosed. That’s because the same error message can have four or five completely different root causes.

Here’s what it looks like across browsers:

Chrome:

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

Firefox:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote
resource at https://api.i-hate-cors.com/data.
(Reason: CORS header 'Access-Control-Allow-Origin' missing).

Safari:

Origin https://i-hate-cors.com is not allowed by Access-Control-Allow-Origin.

All three mean the same thing: your JavaScript made a cross-origin request, the response came back, and the browser looked for an Access-Control-Allow-Origin header in that response. It wasn’t there. So the browser blocked your code from reading the response.

But why the header is missing, that’s where it gets interesting.

Cause 1: The server doesn’t send CORS headers at all

The most straightforward cause, and the most common for developers setting up a new project. Your server simply isn’t configured to include CORS headers in its responses.

This is what’s happening:

Browser → sends request with Origin: https://i-hate-cors.com
Server  → processes request, returns response (no CORS headers)
Browser → looks for Access-Control-Allow-Origin, doesn't find it, blocks the response

Your server did its job. It received the request, handled it, and sent back a valid response. But it didn’t include the one header the browser needs to see before it’ll let your JavaScript read that response.

How to confirm: Open DevTools → Network tab → click the failed request → look at the Response Headers. If there’s no Access-Control-Allow-Origin header anywhere in the response, this is your cause.

The fix: Configure your server to include the header. The exact implementation depends on your stack:

// Express
const cors = require('cors');
app.use(cors({ origin: 'https://i-hate-cors.com' }));
# Flask
from flask_cors import CORS
CORS(app, origins=['https://i-hate-cors.com'])
// Go net/http
w.Header().Set("Access-Control-Allow-Origin", "https://i-hate-cors.com")
# Nginx
add_header Access-Control-Allow-Origin "https://i-hate-cors.com";

If you’re just developing locally and want to get past CORS, I don’t recommend using Access-Control-Allow-Origin: *. It will likely create more problems than it solves, particularly if you need to send authenticated requests.

Cause 2: The preflight succeeded, but the actual response doesn’t have the header

This one catches people who think they’ve already fixed the problem.

When the browser sends a preflight OPTIONS request, your server needs to return CORS headers on that response. Many developers configure that correctly. But the browser also checks for Access-Control-Allow-Origin on the actual request’s response — the GET, POST, PUT, or whatever your code is sending.

If your CORS middleware only adds headers to OPTIONS responses and not to regular responses, the preflight will pass, the browser will send the actual request, and then block it anyway because the actual response is missing the header.

How to confirm: Look at the Network tab. You’ll see a successful OPTIONS request (status 200 or 204) with the correct CORS headers, followed by your actual request whose response is missing them.

The fix: Make sure your CORS configuration applies to all responses, not just preflight responses. Most CORS middleware libraries handle this correctly by default, but custom implementations often get it wrong:

// ❌ Only handles preflight
app.options('/api/data', (req, res) => {
  res.set('Access-Control-Allow-Origin', 'https://i-hate-cors.com');
  res.status(204).send();
});

// ❌ The actual GET handler doesn't include CORS headers
app.get('/api/data', (req, res) => {
  res.json({ message: 'hello' });
});
// ✅ Use middleware that applies to all responses
const cors = require('cors');
app.use(cors({ origin: 'https://i-hate-cors.com' }));

app.get('/api/data', (req, res) => {
  res.json({ message: 'hello' });
});

Cause 3: The server errored out and the error response doesn’t have CORS headers

This is a sneaky one. Your CORS configuration is correct. Your server adds the right headers. But then your handler throws an exception, and the error response, the 500 Internal Server Error, doesn’t go through the same middleware that adds CORS headers.

The result: the browser sees a response without Access-Control-Allow-Origin and shows you a CORS error. The real problem is a server bug, but the CORS error masks it.

How to confirm: This can be tricky from the browser alone since the CORS failure prevents you from seeing the response status code in JavaScript. But the Network tab will sometimes show the status code even for CORS-blocked responses. If you see a 500 alongside the CORS error, your server is crashing. The easiest way to confirm it is outside the browser. Just send a curl request to the server and make sure the response matches what you expect.

Also check your server logs. If there’s a stack trace or error log for the same timestamp, that’s your real problem.

The fix: Make sure your error handling middleware runs after your CORS middleware, or that your CORS headers are added regardless of response status:

// ✅ CORS middleware comes first
app.use(cors({ origin: 'https://i-hate-cors.com' }));

app.get('/api/data', (req, res) => {
  // Even if this throws, the CORS middleware
  // has already set up the response headers
  throw new Error('something broke');
});

// Error handler comes last
app.use((err, req, res, next) => {
  res.status(500).json({ error: 'Internal server error' });
});

The order matters. If your CORS middleware is downstream of the error handler, error responses will bypass it entirely.

Cause 4: A redirect is stripping the headers

Your server responds with a 301 or 302 redirect. The browser follows the redirect to the new URL. But the response from the new URL doesn’t include CORS headers either because it’s a different server that isn’t configured for CORS, or because the redirect itself doesn’t preserve them.

This is common with:

  • HTTP → HTTPS redirects (your server forces HTTPS, but the redirect response or the HTTPS endpoint doesn’t have CORS headers)
  • Trailing slash redirects (/api/data redirects to /api/data/)
  • Load balancers or CDNs that redirect to canonical URLs

How to confirm: In the Network tab, look for a 301 or 302 response before the final response. Click on the redirected request and check its response headers. If the CORS headers are missing from the final response in the redirect chain, that’s your cause.

The fix: Either configure the redirect destination to include CORS headers, or update your frontend to use the final URL directly (skipping the redirect entirely). For trailing slash issues, make sure your fetch() URL matches exactly what the server expects.

Cause 5: Infrastructure is eating the header

Your application code sets the header correctly. You can verify it by hitting the server directly (with curl or Postman). But when the request goes through your production infrastructure (reverse proxy, CDN, API gateway, load balancer), the header disappears.

This happens more often than you’d expect:

  • Nginx or Apache can be configured to strip or overwrite headers
  • CDNs like CloudFront may not forward the Origin request header to your server, so your server doesn’t know to include CORS headers in the response
  • API Gateways (AWS API Gateway, Azure API Management) sometimes need separate CORS configuration at the gateway level, independent of your application code
  • Caching layers may serve a cached response that was originally generated for a different origin (or no origin at all)

How to confirm: If CORS works when you access your server directly but breaks when going through your production URL, infrastructure is the likely culprit. Compare the response headers from a direct server request vs. the production URL.

The fix: This depends on your infrastructure, but the debugging approach is always the same: trace the request from the browser to the server and find where the header gets dropped. Start from the outside (the response the browser receives) and work inward.

Putting it together

When you see “Access-Control-Allow-Origin missing,” resist the urge to immediately change server configuration. Instead, take 30 seconds to figure out which of these causes you’re dealing with:

  1. Check the Network tab. Is there a preflight? Did it succeed or fail?
  2. Look at the response headers. Is the header missing from the preflight response, the actual response, or both?
  3. Check the response status. Is it a 200? A 500? A redirect?
  4. Compare environments. Does it work when hitting the server directly? Does it work locally but not in production?

Each answer points you to a different root cause and a different fix. The error message is the same in all five cases, but the solution is completely different.

Try it yourself

This error (“Access-Control-Allow-Origin missing”) is exactly what the free demo lab covers. You’ll trigger the error in a live environment, inspect the request and response, add the correct header using the built-in CORS widget, and watch the browser accept the response.

It takes about 10 minutes and you don’t need to install anything.

Next up in the series: CORS error: Access-Control-Allow-Origin does not match — when the header is there but the value is wrong, and the surprisingly many ways that can happen.


This post is part of the CORS error series from I-Hate-CORS. The course covers all of these errors with hands-on labs where you can reproduce, debug, and fix each one in a live environment.