# How I debug CORS without guessing

A step-by-step, beginner-friendly guide to understanding CORS errors, checking response headers, and using the CORS Checker correctly.

- Date: 2026-09-27
- URL: https://ilham.dev/posts/how-to-debug-cors-without-guessing/
- Markdown: https://ilham.dev/posts/how-to-debug-cors-without-guessing/index.md
- Tags: cors, api, web, tools
- Reading time: 5 min


CORS errors are frustrating because the browser often shows a scary message, but the
real cause is usually a small missing header.

You may see something like this in the browser console:

```text
Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy.
```

The message sounds complicated. In plain language, the browser is saying:

> “This website is trying to read data from another website. I will only allow it
> if the API explicitly says this website is allowed.”

The [CORS Checker](/tools/cors-checker/) helps you test that permission step.

## What CORS is, in simple terms

CORS stands for Cross-Origin Resource Sharing.

An **origin** is the combination of:

- protocol, such as `https`;
- domain, such as `app.example.com`;
- port, such as `443` or `3000`.

These are different origins:

```text
https://app.example.com
https://api.example.com
http://app.example.com
http://localhost:3000
http://localhost:5173
```

Even if the domains look similar, the browser treats them as different origins.

CORS is a browser safety rule. It does not stop your server from receiving the
request. It stops JavaScript in the browser from reading the response unless the API
allows it.

## The common beginner mistake

When people see a CORS error, they often think:

> “The API is down.”

But that is not always true.

The API might be working. The request might even reach the API. The browser is only
blocking the frontend from reading the response because the response does not have
the right CORS headers.

That difference matters.

## Step 1: identify the frontend origin

First, write down where your frontend is running.

Examples:

```text
http://localhost:5173
https://app.example.com
https://admin.example.com
```

This is the origin the API must allow.

Be exact. These are not the same:

```text
http://localhost:3000
http://localhost:5173
https://localhost:5173
```

Different port means different origin. Different protocol means different origin.

## Step 2: identify the API URL

Next, write down the exact API URL being called.

Example:

```text
https://api.example.com/v1/users
```

Do not only check the base domain. CORS behaviour can differ by route, method, or
server layer.

For example:

```text
https://api.example.com/v1/users
https://api.example.com/v1/admin/users
```

may not be handled by the same backend code.

## Step 3: open the CORS Checker

Open [CORS Checker](/tools/cors-checker/) and enter the API URL.

The tool will try to make a browser request and show what the browser can see. This
is important because CORS is enforced by the browser. Testing from a terminal with
`curl` is useful, but it does not behave exactly like browser JavaScript.

If the request is blocked, the checker helps confirm that the browser cannot read
the response.

## Step 4: look for Access-Control-Allow-Origin

The most important header is:

```http
Access-Control-Allow-Origin: https://app.example.com
```

or sometimes:

```http
Access-Control-Allow-Origin: *
```

This tells the browser which origin is allowed to read the response.

If your frontend is running at:

```text
https://app.example.com
```

then this is good:

```http
Access-Control-Allow-Origin: https://app.example.com
```

This is not good enough:

```http
Access-Control-Allow-Origin: https://www.example.com
```

The browser needs an exact match, unless the API uses `*` for public non-credential
requests.

## Step 5: check whether credentials are involved

Credentials usually means cookies, HTTP auth, or requests with
`credentials: "include"`.

If the frontend sends credentials, the API cannot use this:

```http
Access-Control-Allow-Origin: *
```

It must return the specific origin:

```http
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
```

This is a common source of confusion. `*` feels like it should allow everything, but
browsers reject it for credentialed requests.

## Step 6: understand preflight requests

Some requests trigger a preflight request. A preflight is the browser asking for
permission before sending the real request.

The browser sends an `OPTIONS` request first when the real request is not considered
simple. This can happen when you use:

- methods like `PUT`, `PATCH`, or `DELETE`;
- custom headers like `Authorization`;
- content types like `application/json` in some cases.

The API must answer the preflight with headers such as:

```http
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
```

If the preflight fails, the real request may never be sent by the browser.

## Step 7: check allowed methods

If your frontend sends:

```http
DELETE /v1/users/123
```

then the preflight response must allow `DELETE`:

```http
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
```

If the server only allows:

```http
Access-Control-Allow-Methods: GET, POST
```

then the browser will block the request.

## Step 8: check allowed headers

If your frontend sends an `Authorization` header:

```http
Authorization: Bearer <token>
```

then the preflight response must allow it:

```http
Access-Control-Allow-Headers: Authorization, Content-Type
```

If `Authorization` is missing from `Access-Control-Allow-Headers`, the browser may
block the request before your API handler sees it.

This is why login-protected APIs often fail CORS during development.

## Step 9: separate CORS errors from API errors

These are different problems:

| Symptom | Meaning |
| --- | --- |
| CORS blocked | Browser refused to expose the response. |
| `401 Unauthorized` | API read the request but rejected authentication. |
| `403 Forbidden` | API understood who you are but denied access. |
| `404 Not Found` | The route was not found. |
| `500 Internal Server Error` | The server crashed or failed. |

Sometimes the API returns `500`, but the browser only shows a CORS error because the
error response did not include CORS headers. That makes debugging harder.

A good API should include CORS headers even on error responses.

## Step 10: compare browser test with curl

If the browser fails but `curl` works, that does not automatically mean the browser
is wrong.

`curl` is not restricted by CORS. It can show that the API exists, but it cannot
prove that browser JavaScript is allowed to read it.

Use `curl` to check server behaviour. Use the browser or the
[CORS Checker](/tools/cors-checker/) to check browser permission.

## Common fixes

On the API side, the fix is usually one of these:

- add the frontend origin to the allowed origins list;
- allow the HTTP method being used;
- allow headers such as `Authorization` and `Content-Type`;
- return CORS headers for `OPTIONS` preflight requests;
- return CORS headers on error responses too;
- avoid `Access-Control-Allow-Origin: *` when using credentials.

The exact configuration depends on your backend, framework, proxy, and hosting
setup.

## My CORS checklist

When I see a CORS error, I check:

1. What is the frontend origin exactly?
2. What is the exact API URL?
3. Is the browser sending credentials?
4. Is there a preflight `OPTIONS` request?
5. Does the response include `Access-Control-Allow-Origin`?
6. Does the allowed origin exactly match the frontend origin?
7. Are the method and headers allowed?
8. Do error responses also include CORS headers?

CORS feels mysterious until you remember the simple rule:

> The browser will only let frontend JavaScript read a cross-origin response if the
> API response says that origin is allowed.

The [CORS Checker](/tools/cors-checker/) is just a faster way to see that decision
without guessing.
