Skip to content
stateless.co · Engineering notes from the request/response layer
statelessThe engineering desk

A publication about the machinery under everyday software: the contracts between services, the queries behind a page, and the failures that only show up in production.

02Web Platform

CORS Preflight Requests Matter - The Browser Blocks Prods BUT NOT Your Local

The quick test looks great: your browser client sends the request, the dev server responds, and your UI updates happily. Delighted, you release into production. Minutes later, users are…

CORS Preflight Requests Matter - The Browser Blocks Prods BUT NOT Your Local
Photo: Shenzybaby · CC BY-SA 4.0 · Wikimedia Commons
What is in this piece
  1. The Browser is Failing Before Your App Code
  2. When Access-Control-Allow-Origin is missing or wrong
  3. Authentication Headers
  4. Why Production-Only Proxies and Gateways Break the Preflight
  5. A CDN might strip or replace origin headers
  6. Cache and Redirect Traps
  7. The Practical Mapping
  8. "OPTIONS blocked" might indicate a server security rule
  9. Proxies might fail when they alter the Origin header
  10. Authentication should always pass preflight
  11. A secure CORS policy should include the Referer header
  12. Preview Environments can break CORS

The browser did. Browsers can allow your specific application once it runs in dev, but then block the same application in production when it acknowledges it's running on an external server.

When making requests, the browser checks two things:

  1. Is the request URL whitelisted by your site's CORS settings?
  2. Is the request type (method, headers) allowed by the URL's CORS policy?

For a normal browser request, these CORS checks are made before the main HTTP request—usually, with OPTIONS request, rather than GET or POST—the browser will see this during standard CORS preflight.

However, we're looking here at a production-service preflight, where a bot finds and blocks a potential abuse path before it even reaches your service.

The Browser is Failing Before Your App Code

We know, you tried all night: the browser throws a preflight error, despite your server being a working, passing service. How?

Because the browser's CORS enforcement affects the OPTIONS preflight, without considering your server's browser request. It's not making a check to see if your server does the right thing: because it's made it's decision before the request ever gets there.

A CORS preflight request is an OPTIONS request made with the headers:

  • Origin
  • Access-Control-Request-Method
  • Access-Control-Request-Headers, optionally

The Expected CORS preflight is made before the main HTTP request, and will check to see if the signature headers pasted into the preflight are on a whitelist for that origin's preflight.

Here's the actual text your browser might show:

Preflight Request Issue

This originates from the browser comparing the url in the referer header of the CORS preflight with known local server paths. For localhost, this is usually covered. But there are several production strategies that can rule out servers as having legitimate CORS rules, such as distribution, caching and request forwarding.

When Access-Control-Allow-Origin is missing or wrong

The browser knows to make the comparison, and reject the preflight, when it detects that any server not actually an origin whitelisted by the current site's domain has answered.

So, the user experience here is that the Access-Control-Allow-Origin is set for:

  • a different domain than the current
  • a wildcard domain, which browsers block for instances requiring credentials

Without that credential check, the browser is forced to reject access from your application without even trying your server request.

Here's one common browser error message:

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

This happens when the requested URI and the referer URI don't share the same domain.

You might see this in production when your CORS preflight request forwards to a trusted, but different, subdomain. Or, when your development machines are using different domain as your customer facing servers

Often credential checks are missing, but configured in a way that would normally include credentials. Here, the servers might:

  • Allow any url except those requesting basic auth
  • Allow wildcard domains, but block

One other sneaky credential error could appear if neither of these checks are make, and the origin header is blanked by the server:

The required HTTP header '`Access-Control-Allow-Origin`' is not returned.

Some domains might not be aware that they should supply that return header, or might strip headers in a security misconfiguration. It can lead to blocked requests.

Authentication Headers

Even if you accept credentials, and pass some authentication headers down, you can still get rejected if:

  • the headers your endpoint uses are not individually added to the Access-Control-Allow-Headers whitelist
  • there's an authentication mix up, with included headers showing up on both the preflight and main request

Again, the solution here is to take a system look: which server is responding, are the credentials turned off, does the naming match, and is the endpoint-public to this origin?

Why Production-Only Proxies and Gateways Break the Preflight

The browser's first line is crucial here: it can also catch unwanted preflight requests made in many environments.

  • ISPs, commonly with DNS settings, resolve these to a blocked, rejected proxy server that blocks simply on origin.
  • Security middlewares, or content delivery networks (CDNs), will commonly check origins for whitelisting
  • Anonymizing proxies, rewriting referral urls are especially risky, removing key headers that the browser uses to determine whether to block

The error here is that your application has no idea about these interceptor nodes: the browser will show an error something like this:

Origin: your.url provided in a resolved header,
 new:proxy.org was in the CORS preflight request
The server at your.url did not send any 'Access-Control-Allow-Origin' header
 even though it does not allow credentials

A CDN might strip or replace origin headers

On CDNs, it is possible that either:

  • Origin wasn't passed in as a header to begin with
  • Or it was passed, but stripped out or remapped to match something else

This produces a conundrum for the browser, which believes that your trusted domain has become a totally different, masquerading origin. So the CORS block you know from the proxy is actually happening in the browser before the request reaches that server.

The 'Access-Control-Allow-Origin' header has a value 'new.server.org'
 that is not equal to the supplied origin 'your.app.distributor.org'

This is especially a problem with regional CDN agents. Sometimes the headers that are passed in one region might not make it to all agents. One possible solution is to add an Origin header manually in your preflight handler.

Cache and Redirect Traps

When your backend is sitting behind a CDN or load balancer, the preflight is a great opportunity for it to cache as much as possible. Great: it lessens the load on your server.

However, many CDNs do not correctly cache on CORS headers: often, the direction is removed. This causes a CORS block, just as if your backend had used a wildcard bypass on an origin block.

Additionally, your CDN might accidentally redirect OPTIONS requests. Your CDN may be smart: it ignores OPTIONS requests, because it doesn't expect those to be passed through.

This can happen with one contact violation:

  • CORS policies aren't regularly updated

If you changed those between versions, make sure the CDN caches them correctly.

One CDN caching issue for CORS is that the Origin header must always correctly set via a cache policy. If Origin is removed, your preflight will be blocked, and appear as if your origin is unauthorized.

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

The Practical Mapping

There is no single error:

"Reason: CORS preflight channel did not succeed"

rather, you have to look at both the server name and browser rejection message

For example, you might see:

"OPTIONS blocked" might indicate a server security rule

Some servers block OPTIONS from unauthorized origins, instead of including them in the check. This can result in a preflight server response showing:

Some security gateways block preflights before they make it to the app layer. For example, for app servers with a well-known dev Cisco firewall set to allow only certain GET/PUT/POST verbs.

Make sure your preflight originates from your servers, and that your firewall rules allow for these requests:

servername was blocked by CORS policy

request didn't pass security check, so OPTIONS blocked

Proxies might fail when they alter the Origin header

The true nature of these messages can only be found by debugging and checking the logs - on both CDNs and proxies - but they should result in one of these messages:

Request Origin header did not match sender's.

Authentication should always pass preflight

The browser uses the OPTIONS preflight to see if the actual request will pass. If the preflight fails, the browser will reject the actual request.

For example, you might be passing a custom Authorization header or CORS credentials, read this:

Credentials are not supported if the 'Access-Control-Allow-Origin' header is '*'.

And this:

Access-Control-Allow-Origin missing
Actual server header is `yourapi.oracle.com`

This would indicate that you've set up an explicit origin rule, but "yourapi.oracle.com" is not included, or rules aren't updated.

Preflight should be allowed for all requests, regardless of credentials. Additionally, every nginx worker process should have it's own cache - if you're seeing blocked requests due to "preflight nets out early", this could be a cache miss.

A secure CORS policy should include the Referer header

Many CSPs don't allow for Referer Header. It's been removed by many browsers, but should be included:

This is not valid as part of CORS;

  • the header is whitelisted, and should work as-is
  • Remove any restrictions on Referer

Preview Environments can break CORS

Most browser behavior is asynchronous, buffered until the preflight resolves. This can cause a CORS request which is valid in production, or even staging, to appear as failures on dev or preview.

Most common is when a site is served over a temporary origin like preview.venv.vercel.app or Example preview domain, a dev-only node. Generally these nodes are used temporarily, but are under the same target domain as the main site.