Learn how to debug a CORS error.

•

•

The Browser Is Blocking You for a Reason

I can still hear the low hum of my laptop fan from that Tuesday night three years ago, sitting in a dark apartment while a single red error message mocked me from the DevTools console. I had just pushed a “simple” configuration change to the gateway, only to watch our frontend integration go dark because of a single missing header. Most tutorials online will try to sell you on complex middleware wrappers or fancy security libraries, but they miss the point of how to debug a cors error when you’re actually in the middle of a deployment. It isn’t about learning a new framework; it’s about understanding the unspoken handshake between the browser and your server that you just accidentally interrupted.

I’m not here to give you a theoretical lecture on the Same-Origin Policy or a list of academic definitions. Instead, I want to walk you through the actual, messy process of tracing a failed preflight request and identifying exactly where the communication broke down. I’ll show you the specific headers to watch, the common mistakes I’ve made in production, and how to isolate the culprit without wasting four hours of your life.

Table of Contents

Why Your Fetch Api Suddenly Stopped Working

Why Your Fetch Api Suddenly Stopped Working

Usually, when my `fetch` calls start failing, it isn’t because the network is down or the endpoint is returning a 500. It’s because the browser decided to step in as a chaperone. I’ve seen this happen most often when we add a new custom header to a request or switch from a simple GET to a more complex POST. The browser suddenly decides the request is “non-simple” and triggers an options request preflight failure. It sends a little probe to the server to ask permission before sending the actual data, and if your server isn’t expecting that probe, it just stays silent or returns a 404.

The real headache comes from the fact that the error message in the console is notoriously unhelpful. It’ll tell you something is wrong, but it won’t tell you why. Most of the time, I’m looking for a missing or malformed access-control-allow-origin header. If the server doesn’t explicitly shout back that your origin is allowed, the browser will kill the connection to protect the user. It feels like fighting a ghost because the code looks perfect on my machine, but the server-side CORS configuration is silently rejecting the handshake. It’s a classic case of the security policy doing exactly what it was designed to do, even if it’s currently making my life miserable.

Tracing the Ghost in the Access Control Allow Origin Header

Tracing the Ghost in the Access Control Allow Origin Header.

Once you’ve ruled out a basic network hiccup, you usually end up staring at the Network tab in your DevTools, hunting for the specific culprit: the `access-control-allow-origin` header. I’ve lost count of how many times I’ve assumed the server was sending the right thing, only to find it was returning a wildcard `*` when my frontend required specific credentials. It’s a classic trap. If you’re sending cookies or authorization headers, a simple wildcard won’t cut it; the browser’s security policy will kill the request before it even touches your application logic.

Usually, the real headache isn’t the actual GET or POST request, but the options request preflight failure that happens right before it. This is where the browser sends a little “scout” request to see if the server is willing to play ball. If your server-side CORS configuration isn’t explicitly set up to handle these `OPTIONS` methods, the preflight fails, and the actual data request never even leaves your machine. I once spent an entire afternoon debugging a “broken” API, only to realize my load balancer was stripping out the headers during the preflight phase. It’s rarely a code bug; it’s almost always a misunderstanding of the handshake.

Five ways to stop chasing your tail

  • Stop looking at the JavaScript console first. The browser’s dev tools tell you that it failed, but they are terrible at telling you why. Go straight to the Network tab, find the failed preflight OPTIONS request, and look at the actual response headers. If the OPTIONS request doesn’t return a 200 or 204, your actual GET or POST request isn’t even happening.
  • Beware the “double header” trap. I once spent a whole afternoon debugging a production issue where our load balancer was adding an `Access-Control-Allow-Origin` header and our application was adding another one. The browser sees two values, panics, and blocks everything. Check if your infra and your code are fighting over the same responsibility.
  • Don’t use wildcards in production just to make the error go away. Setting `Access-Control-Allow-Origin: *` is the quickest way to fix a bug, but it’s also the quickest way to create a security hole, especially if you need to support credentials (cookies/auth headers). If `Allow-Credentials` is true, the wildcard is a hard no; you have to be explicit with the origin.
  • Check your custom headers. If you’re sending something non-standard like `X-My-Custom-App-Header`, the browser will trigger a preflight. You can’t just allow the origin; you also have to explicitly list that custom header in the `Access-Control-Allow-Headers` response. If it’s missing from that list, the preflight fails, and you’re back to square one.
  • Remember that CORS is a browser security feature, not a server error. If you try to test your API using `curl` or Postman and it works perfectly, but fails in Chrome, it isn’t a broken endpoint—it’s a CORS configuration issue. Don’t waste time debugging your database or your business logic when the problem is actually just the handshake between the client and the server.

What I’m actually walking away with

Stop guessing at the browser console; if the error looks vague, open the Network tab and look at the actual preflight OPTIONS request. The browser is usually telling you exactly what it’s missing, but it won’t shout it at you in the main error log.

Don’t treat `Access-Control-Allow-Origin: *` as a permanent fix. It’s a band-aid that works until you need to send a cookie or an authorization header, at which point it fails and you’re right back where you started.

Most CORS headaches aren’t actually code bugs—they’re configuration mismatches between what your client expects and what your load balancer or API gateway is actually allowing through. Check the infrastructure, not just your Express middleware.

The Post-Mortem Mindset

Debugging CORS with The Post-Mortem Mindset.

At the end of the day, debugging CORS isn’t about mastering a specific library; it’s about understanding the invisible handshake between the browser and your server. You have to look past the generic red text in the console and actually examine the preflight OPTIONS request to see where the negotiation failed. Whether it was a missing header, a mismatch in the allowed methods, or a credential issue that only appeared in production, the fix is always found in the details of that exchange. I’ve learned that if you can’t see the exact header being sent, you aren’t actually debugging—you’re just guessing. Trust the network tab, not your assumptions.

I used to view these errors as personal failures or just annoying hurdles in my sprint. Now, I see them as necessary friction that forces us to actually understand how the web works. Every time I spend an hour untangling a header mismatch, I’m building a mental map that will save me from a much worse outage down the road. Don’t let the frustration of a broken fetch request get to you. Instead, treat every production error as a free lesson in distributed systems. Write down the fix, commit the configuration change, and move on to the next problem. That’s how we actually get better.

Frequently Asked Questions

If I see a CORS error in the console, how do I know if the problem is actually my frontend code or if the backend is just refusing to play nice?

The quickest way to stop guessing is to look at the Network tab, not the Console. If the request shows a status of `(failed)` or `CORS error` without even hitting a 4xx or 5xx code, the browser likely killed the preflight `OPTIONS` request before it even reached your application logic. If you see a 200 OK but the console still screams, your backend is technically responding, but it’s lying about which origins it trusts.

Why does it work perfectly fine on my local machine but immediately blow up the second I push to the staging environment?

It’s almost always the environment configuration. On localhost, you’re usually playing by different rules—either everything is on the same port, or you’ve got a proxy masking the origin. The second you push to staging, you’re hitting real DNS, real HTTPS, and real load balancers. Your staging server is likely seeing a different `Origin` header than your local machine ever did, and it’s rightfully refusing to play ball.

Is it actually safe to just use a wildcard `*` for my origin in production, or am I just asking for a security incident?

Look, I’ve seen the “just use a wildcard” fix in a dozen PRs, usually by someone who was tired of fighting the browser. It works, but it’s a lazy shortcut. If you use `*`, you’re essentially telling the browser, “I don’t care who calls this API.” This means you can’t use credentials like cookies or Authorization headers. If your data is public, fine. If it’s sensitive, you’re just waiting for a security audit to go sideways.

How do I debug this when the error message is totally useless and doesn't even tell me which specific header is missing?

When the browser gives you that generic “CORS error” without specifics, stop looking at the console and start looking at the Network tab. Click the failed request and check the “Headers” sub-tab. If the request didn’t even reach the server, you’re looking at a preflight (OPTIONS) failure. If it did, compare the request headers against the response headers. Usually, the culprit is a missing `Access-Control-Allow-Headers` entry for a custom header you just added.

Tomasz Wierzbicki

About Tomasz Wierzbicki

I am not interested in arguing about frameworks. I am interested in why the thing broke at two in the morning and what would have prevented it. Most of what I know came from being wrong in production and writing it down afterwards.