Back to Blog
February 15, 2026 Part 3

CORS error: Access-Control-Allow-Origin does not match

This error is more frustrating than a missing header because the server looks CORS-aware, but the value still fails strict origin matching.

I
I-Hate-CORS team
Author
CORS error: Access-Control-Allow-Origin does not match

This is the third post in our CORS error series. The previous post covered what happens when the header is missing entirely. This one covers what happens when it’s there but wrong.


This error is more frustrating than a missing header, because you can see that the server is trying to cooperate. The Access-Control-Allow-Origin header is present. The server is CORS-aware. Something is configured. It just doesn’t match.

Here’s what the browsers tell you:

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: The 'Access-Control-Allow-Origin' header has a value
'https://www.i-hate-cors.com' that is not equal to the supplied origin.

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' does not match 'https://i-hate-cors.com').

Safari:

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

Chrome is the most helpful here. It actually shows you both values: what the server returned and what the browser expected. If you’re debugging this error, use Chrome’s message as your starting point.

The core rule is simple: the value of Access-Control-Allow-Origin must be an exact string match with the Origin header the browser sent. Not a partial match. Not “close enough.” Exact.

Let’s walk through the ways this goes wrong.

Cause 1: www vs. non-www

The classic. Your frontend is at https://i-hate-cors.com and your server returns:

Access-Control-Allow-Origin: https://www.i-hate-cors.com

Or the reverse. These are different origins. The browser will reject it.

This is easy to miss because you probably think of them as “the same site,” and for humans they are. For the browser’s origin comparison, they aren’t. The origin is defined as the combination of scheme + host + port. i-hate-cors.com and www.i-hate-cors.com are different hosts.

The fix: Check which origin your frontend actually runs on. Open DevTools, go to the Console, and type window.location.origin. Whatever that returns is what your server needs to match. If you support both www and non-www, your server needs to read the incoming Origin header and echo it back (after validating it against an allowlist).

Cause 2: Scheme mismatch (http vs. https)

Origin: https://i-hate-cors.com
Access-Control-Allow-Origin: http://i-hate-cors.com

Same domain, different scheme. Different origin. Blocked.

This typically happens during the transition to HTTPS. Your server configuration still has the old http:// value hardcoded. Or your development environment uses HTTP while your CORS config was written for production HTTPS.

The fix: Update the value to match the scheme your frontend actually uses. In production, this should almost always be https://.

Cause 3: Port number differences

During local development, this is one of the most common mismatches:

Origin: http://localhost:3000
Access-Control-Allow-Origin: http://localhost:8080

Your React dev server runs on port 3000. Your API runs on port 8080. You configure CORS to allow localhost:8080 because that’s “your server.” But the browser’s origin is where the frontend is running, not where the API is.

Port is part of the origin. localhost:3000 and localhost:8080 are different origins. localhost:3000 and localhost (no port, implying 80) are also different origins.

The fix: Set Access-Control-Allow-Origin to the origin of your frontend, not your API:

// ❌ This is the API's own address
app.use(cors({ origin: 'http://localhost:8080' }));

// ✅ This is where the frontend runs
app.use(cors({ origin: 'http://localhost:3000' }));

Cause 4: Trailing slash

This one is subtle enough that you can stare at the two values and not see the difference:

Origin: https://i-hate-cors.com
Access-Control-Allow-Origin: https://i-hate-cors.com/

That trailing / makes them different strings. The comparison fails. The browser doesn’t trim or normalize the values before comparing.

How to spot it: Copy both values from the Network tab into a text editor and compare them character by character. Or paste them into the browser console:

'https://i-hate-cors.com' === 'https://i-hate-cors.com/'
// false

The fix: Origins never include a trailing slash. Remove it from your server configuration.

Cause 5: Hardcoded origin when you need multiple

Your API serves two frontends: https://i-hate-cors.com and https://admin.i-hate-cors.com. You hardcode one:

Access-Control-Allow-Origin: https://i-hate-cors.com

Requests from the admin panel fail because the header doesn’t match https://admin.i-hate-cors.com.

The CORS spec doesn’t allow multiple origins in the header. You can’t do this:

# ❌ This is not valid
Access-Control-Allow-Origin: https://i-hate-cors.com, https://admin.i-hate-cors.com

The fix: Your server needs to dynamically set the header based on the incoming request’s Origin value. Read the Origin header, check it against your allowlist, and if it matches, echo it back:

const allowedOrigins = [
  'https://i-hate-cors.com',
  'https://admin.i-hate-cors.com'
];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  next();
});

An important detail when you do this: you must also send a Vary: Origin header. This tells caches that the response changes depending on the Origin header. Without it, a CDN might cache the response with one origin value and serve it to a request from a different origin.

res.setHeader('Vary', 'Origin');

Most CORS middleware libraries handle this automatically, but if you’re implementing it manually, forgetting Vary can cause intermittent CORS failures that are extremely hard to reproduce.

Cause 6: Caching is serving the wrong origin

This one can make you feel like you’re losing your mind. The CORS configuration is correct. You’ve verified it. But the browser still gets the wrong Access-Control-Allow-Origin value, and it seems to change unpredictably.

Here’s what’s happening: a caching layer (CDN, reverse proxy, or browser cache) is serving a cached response that was originally generated for a different origin.

The scenario:

  1. User A visits from https://i-hate-cors.com. Your server returns Access-Control-Allow-Origin: https://i-hate-cors.com. The CDN caches this response.
  2. User B visits from https://admin.i-hate-cors.com. The CDN serves the cached response, which still has Access-Control-Allow-Origin: https://i-hate-cors.com. Mismatch. CORS error.

The fix: This is the Vary: Origin header mentioned above. It tells the cache to store separate versions of the response for each unique Origin header value. Without it, the cache treats all requests to the same URL as identical regardless of who’s asking.

If you’re using a CDN like CloudFront, you also need to configure it to forward the Origin request header to your server. By default, some CDNs strip it.

The debugging approach

When you see this error, the fix is almost always visible in the Network tab. Here’s the fastest path:

1. Click the failed request in the Network tab.

2. Look at the Response Headers. Find Access-Control-Allow-Origin. Copy the value.

3. Look at the Request Headers. Find Origin. Copy the value.

4. Compare them. The mismatch will be one of: www vs. non-www, http vs. https, port difference, trailing slash, or a hardcoded value that doesn’t match this particular origin.

Chrome’s error message actually shows both values, which saves you steps 2 and 3. Use it.

5. If the values look identical, check for invisible characters. Copy both into a JavaScript console and compare with ===. Sometimes encoding issues or invisible Unicode characters cause mismatches that look correct visually.

6. If the mismatch seems to change between requests, suspect caching. Check whether Vary: Origin is present in the response.

Try it yourself

The I-Hate-CORS course has a dedicated lab for this error where the server returns an Access-Control-Allow-Origin value that doesn’t match your origin. You’ll inspect the mismatch, use the CORS configuration widget to correct it, and watch the browser accept the response.

Next up in the series: CORS error: Credential is not supported if Access-Control-Allow-Origin is * - the interaction between credentials and wildcards that surprises almost everyone the first time they hit it.


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.