401 vs 403 vs CORS: How to Know What Is Actually Broken

API Debugging Guide

401 vs 403 vs CORS: How to Know What Is Actually Broken

A browser API request fails and you see 401, 403, or a scary CORS message. The hard part is not memorizing the status codes. The hard part is knowing which layer failed first. This guide explains how to separate authentication problems, permission problems, and browser security problems so you do not waste hours fixing the wrong thing.

Quick answer: A 401 usually means the API does not accept your identity yet. A 403 usually means the API knows who you are but refuses the action. A CORS error means the browser did not receive permission to expose the response to your frontend code. CORS can hide the real 401 or 403 underneath, so always check the browser Network tab before changing your auth logic.

401 Unauthorized 403 Forbidden CORS Preflight OPTIONS Bearer tokens Cookies Fetch and Axios

Fast decision map: 401 vs 403 vs CORS

Start with this simple map before you change any code. Many beginners see the word “unauthorized” and assume every auth-looking error is the same. That is where the confusion starts.

Symptom Plain-English meaning Most likely layer First thing to check
401 Unauthorized The API does not have valid authentication for this request. Authentication Missing token, expired token, wrong token format, wrong login cookie, missing WWW-Authenticate clue, wrong environment.
403 Forbidden The API understood the request but refuses this user, role, origin, plan, IP, or action. Authorization or policy User role, permission, resource ownership, API plan, feature flag, server rule, WAF, or blocked IP.
CORS console error The browser blocked frontend JavaScript from reading the cross-origin response. Browser security and server CORS headers Access-Control-Allow-Origin, preflight OPTIONS, allowed methods, allowed headers, credentials.
OPTIONS returns 401 Auth middleware is blocking the browser’s permission check before the real request. CORS preflight plus backend middleware Let preflight pass and return the correct CORS headers before requiring auth on the actual request.
OPTIONS returns 403 The server, gateway, WAF, or route policy rejects the browser’s preflight request. CORS, gateway, or security policy Allowed origin, allowed method, allowed request headers, blocked origin, hosting rules.

The important difference is this: 401 and 403 are HTTP responses from the server. CORS is a browser access decision based on server response headers. Sometimes you will see both at once because the server returned a real error, but the browser refused to expose the body to JavaScript.

First question: did the browser receive a readable API response?

Do not start by changing the bearer token, rewriting the fetch call, or blaming React. Open DevTools and inspect the request in the Network tab. The console message is often only a summary. The Network tab tells you whether the browser sent a preflight request, whether the real request was sent, and whether the response was readable by JavaScript.

1

Look for an OPTIONS request

If you see an OPTIONS request before your real GET, POST, PUT, or DELETE, the browser is doing a CORS preflight check. If that request fails, the real API request may not run at all.

2

Check the status of the failed row

If the failed row is OPTIONS, fix CORS and preflight handling first. If the failed row is the real API method, then debug the actual status code, such as 401, 403, 404, 415, 422, or 500.

3

Read the response headers

For cross-origin browser requests, the server response needs the right CORS headers. A JSON error body alone is not enough if the browser is not allowed to expose it.

4

Compare browser and Postman carefully

Postman may send the token, cookies, or headers differently. Browser fetch may trigger preflight, omit cookies unless configured, or get blocked by CORS before JavaScript can read the response.

Practical rule: If the browser says CORS and your backend logs show no real POST or PUT, do not debug the JSON body yet. The real request may never have reached that route.

What 401 Unauthorized really means

A 401 Unauthorized response usually means the API needs valid authentication and did not receive it. The confusing part is the word “Unauthorized.” In beginner terms, treat 401 as “not successfully logged in for this request.” It usually points to identity, token, session, or credentials.

Common 401 causes

  • The Authorization header is missing.
  • The token is expired, revoked, malformed, copied incorrectly, or from the wrong environment.
  • The request uses BearerTOKEN instead of Bearer TOKEN with a space.
  • The frontend sends an API key in the wrong header name.
  • The login cookie is not being sent because fetch credentials are not configured.
  • The browser is calling the production API with a localhost or staging token.
  • The API expects Basic auth but the frontend is sending a bearer token.
  • A reverse proxy or gateway strips the Authorization header before the app receives it.

Example of a correct bearer token request in browser fetch:

const response = await fetch("https://api.example.com/v1/profile", {
  method: "GET",
  headers: {
    "Authorization": "Bearer YOUR_TOKEN",
    "Accept": "application/json"
  }
});

Example of a 401-style response body:

{
  "error": "unauthorized",
  "message": "Missing or invalid access token"
}

A 401 should make you ask: “Did the API receive the correct identity proof?” That identity proof might be a bearer token, session cookie, API key, Basic auth value, signed request, or OAuth access token.

Debugging shortcut: Copy the exact request as cURL from your browser Network tab and compare it with the cURL request that works in Postman. Look for missing Authorization, different host, different token, missing cookie, or a changed Accept header.

What 403 Forbidden really means

A 403 Forbidden response usually means the server understood the request but refuses to process it. In beginner terms, treat 403 as “the API knows enough to say no.” The user may be logged in, but the account, role, plan, IP, origin, or resource permission does not allow this action.

Common 403 causes

  • The user is authenticated but does not have the required role, such as admin, owner, editor, or paid user.
  • The token is valid but missing the required scope.
  • The user is trying to access another user’s resource.
  • The API key is valid but not allowed for that endpoint.
  • The account plan does not include the feature.
  • The API blocks requests from a region, IP range, domain, referrer, or origin.
  • A WAF, firewall, CDN, or hosting security rule blocks the request.
  • A WordPress nonce, CSRF token, or capability check fails.

Example of a 403-style response body:

{
  "error": "forbidden",
  "message": "You do not have permission to delete this project"
}

A 403 should make you ask: “Assuming the identity is known, is this identity allowed to do this specific action on this specific resource?”

Good API design habit: A useful 403 response should not expose private details, but it should be clear enough for legitimate developers. For example, “Missing required scope: projects:delete” is more helpful than only saying “Forbidden.”

What CORS really means

CORS is not the same thing as authentication. CORS is about whether browser JavaScript from one origin can access a response from another origin. Your request can have a perfect token and still fail if the API does not allow your frontend origin.

These are different origins:

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

If your frontend is running at http://localhost:5173 and your API is at https://api.example.com, the browser treats that as cross-origin. The API must return headers that allow the requesting origin to access the response.

Example CORS response for a browser frontend:

Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, Accept

Preflight makes this more confusing

For many API requests, the browser sends an OPTIONS request before the real request. This preflight asks the server whether the actual method and headers are allowed.

Example preflight request:

OPTIONS /v1/projects HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type

Example successful preflight response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600

If your auth middleware rejects that OPTIONS request with 401, the real POST may never happen. That is why a CORS problem can look like an auth problem and an auth problem can appear inside a CORS message.

Common mistake: Adding Access-Control-Allow-Origin to your frontend request headers does not fix CORS. The browser needs that header on the server response.

How CORS can hide a real 401 or 403

Sometimes the server really does return a 401 or 403, but your frontend code cannot read the response body because CORS headers are missing. You may see a CORS console error instead of the useful JSON message.

For example, the server might return this:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": "unauthorized",
  "message": "Token expired"
}

But if the response does not include the correct Access-Control-Allow-Origin header for the browser origin, JavaScript may not be allowed to read the JSON body. The console may focus on CORS, even though the underlying API problem is an expired token.

This is why the Network tab matters. It can show you the response status and headers even when your frontend code only receives a generic fetch failure.

What you see What may be happening underneath Best next move
CORS error, no readable response in JavaScript The server did not allow your frontend origin. Check response headers and allowed origins.
CORS error plus OPTIONS 401 Authentication middleware is blocking preflight. Allow unauthenticated OPTIONS preflight to return CORS headers.
CORS error plus GET 401 The real request happened, but token or cookie is invalid and CORS headers may be missing on error responses. Fix token and make sure error responses also include CORS headers.
CORS error plus GET 403 The request may be forbidden and the browser cannot expose the error body. Check permissions and CORS headers on 403 responses.

The safest debugging order

Use this order when you are confused. It prevents you from fixing the token when the real problem is preflight, or changing CORS when the real problem is a missing role.

1

Confirm the exact failing URL and origin

Write down the frontend origin and API URL. localhost:3000 and localhost:5173 are different origins. http and https are also different origins.

2

Open the Network tab, not only the console

Find the failed request. Check whether it is OPTIONS or the real request method. This one detail tells you where to start.

3

If OPTIONS failed, debug CORS first

Check allowed origin, allowed methods, allowed headers, credentials, route handling, proxy behavior, and whether auth middleware blocks preflight.

4

If the real request returned 401, debug authentication

Check token presence, token format, expiration, cookie sending, API key header name, OAuth scope format, and environment mismatch.

5

If the real request returned 403, debug permission

Check user role, account plan, resource ownership, endpoint permission, API scope, IP/domain restrictions, WAF rules, and server authorization logic.

6

Make error responses consistent

If your API supports browser clients, make sure 401 and 403 responses also include the required CORS headers. Otherwise the browser may hide the useful JSON error body.

7

Compare Postman, cURL, and browser

A request that works in Postman may still fail in the browser because Postman does not enforce browser CORS rules in the same way. Compare actual headers, cookies, and body values.

Practical examples

Example 1: 401 because the bearer token is missing

Bad request:

const response = await fetch("https://api.example.com/v1/profile", {
  method: "GET",
  headers: {
    "Accept": "application/json"
  }
});

Better request:

const response = await fetch("https://api.example.com/v1/profile", {
  method: "GET",
  headers: {
    "Authorization": "Bearer YOUR_TOKEN",
    "Accept": "application/json"
  }
});

If this changes the result from 401 to 200, the problem was authentication. If it changes the result to a CORS preflight error, then the server may not allow the Authorization header in CORS.

Example 2: CORS because Authorization is not allowed in preflight

The browser asks:

Access-Control-Request-Headers: authorization

The server must allow it:

Access-Control-Allow-Headers: Authorization, Content-Type, Accept

If the server only allows Content-Type but not Authorization, the browser can block the request before your endpoint logic runs.

Example 3: 403 because the user is logged in but lacks permission

This request may include a valid token:

const response = await fetch("https://api.example.com/v1/admin/users", {
  method: "GET",
  headers: {
    "Authorization": "Bearer VALID_NON_ADMIN_TOKEN",
    "Accept": "application/json"
  }
});

But the API may still return:

{
  "error": "forbidden",
  "message": "Admin role required"
}

Changing the token format will not help if the token is already valid but belongs to a user without admin permission.

Example 4: Cookie session works in browser page, fails in fetch

If the API uses cookies, browser fetch may need credentials:

const response = await fetch("https://api.example.com/v1/account", {
  method: "GET",
  credentials: "include",
  headers: {
    "Accept": "application/json"
  }
});

The server also needs to allow credentials correctly:

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

Do not use a wildcard origin for credentialed browser requests. Use the real frontend origin.

React, Vue, Axios, and frontend app notes

The same debugging logic applies whether you use fetch, Axios, React Query, Vue, Angular, or plain JavaScript. The library may change how the request is written, but it does not remove browser security rules.

Axios example with bearer token:

const response = await axios.get("https://api.example.com/v1/profile", {
  headers: {
    Authorization: "Bearer YOUR_TOKEN",
    Accept: "application/json"
  }
});

Axios example with cookies:

const response = await axios.get("https://api.example.com/v1/account", {
  withCredentials: true,
  headers: {
    Accept: "application/json"
  }
});

If Axios says Network Error, do not assume the server is down. It may be a CORS block where the browser refused to expose the response. Open the Network tab and check whether the request, preflight, or response headers tell the real story.

WordPress API debugging note

WordPress users can hit the same problem from custom JavaScript, plugins, shortcode output, custom HTML blocks, or page builder widgets. If the request runs in the visitor’s browser, CORS applies. If the request runs through PHP on the server side, browser CORS is usually not the same issue.

Frontend JavaScript call from a WordPress page:

fetch("https://api.example.com/v1/data", {
  headers: {
    "Authorization": "Bearer PUBLIC_OR_USER_TOKEN",
    "Accept": "application/json"
  }
});

Server-side WordPress request:

$response = wp_remote_get('https://api.example.com/v1/data', array(
  'headers' => array(
    'Authorization' => 'Bearer SERVER_SIDE_TOKEN',
    'Accept' => 'application/json',
  ),
  'timeout' => 20,
));

For private API keys, server-side WordPress code is usually safer than exposing the key in public JavaScript. Browser users can inspect JavaScript, network requests, and visible tokens.

Security reminder: Do not hide a private API key in frontend environment variables and assume it is secret. If it is bundled into browser code or sent in browser requests, users may be able to find it.

Common symptoms and what to check

Symptom Likely issue Check this first
401 Unauthorized after login Token not attached, expired, wrong audience, wrong environment, cookie missing. Actual request headers and cookies in Network tab.
403 Forbidden with valid token User is authenticated but not allowed to perform this action. Role, scope, plan, ownership, endpoint permission.
CORS error with no readable JSON body Server response does not include correct CORS headers. Access-Control-Allow-Origin on the failed response.
OPTIONS 401 Auth middleware blocks preflight. Preflight handling before authentication middleware.
OPTIONS 403 Origin, method, or header rejected by server, proxy, WAF, or hosting layer. Allowed origins, methods, headers, and security rules.
Works in Postman but not browser Browser-only rules such as CORS, preflight, cookies, or mixed origin. Compare Postman cURL with browser Network request.
Works locally but fails after deployment Production origin not allowed, HTTPS mismatch, wrong env variables, production token issue. Actual deployed frontend origin and API base URL.
Only admin endpoints fail Auth works, authorization fails. User role and required scope for the endpoint.

Bad fixes that waste time

Some fixes appear often in forum threads but do not solve the real issue. Avoid these unless you fully understand the tradeoff.

Using mode: "no-cors"

This usually gives your frontend an opaque response it cannot read. It may hide the console error, but it does not give your app usable JSON.

Adding CORS response headers to the request

Access-Control-Allow-Origin belongs on the server response. Adding it to frontend request headers does not grant browser permission.

Changing every 403 to 401

That makes debugging worse. 401 and 403 should tell different stories: missing or invalid identity versus known identity without permission.

Allowing every origin permanently

During local testing, people often open CORS too widely. For real apps, use a clear allowlist and be extra careful with credentialed requests.

Putting private keys in frontend code

Moving a secret token into React, Vue, or public WordPress JavaScript may make a request work, but it can expose the key to users.

Debugging JSON before preflight

If the preflight request fails, your JSON body may not reach the endpoint. Fix the browser permission check first.

Copyable debugging checklist

Use this when you need to explain the issue to a teammate, instructor, support team, or future version of yourself.

API auth and CORS debugging note

Frontend origin:
Example: http://localhost:5173

API URL:
Example: https://api.example.com/v1/projects

Request method:
GET / POST / PUT / PATCH / DELETE

Tool comparison:
Works in Postman? Yes / No
Works in cURL? Yes / No
Fails in browser? Yes / No

Browser Network tab:
Failed row is OPTIONS or real request?
OPTIONS status:
Real request status:
Console error:

Authentication type:
Bearer token / API key / cookie session / Basic auth / OAuth

Request headers:
Authorization:
Content-Type:
Accept:

Credential settings:
fetch credentials or Axios withCredentials:
Cookie present in request? Yes / No

CORS response headers:
Access-Control-Allow-Origin:
Access-Control-Allow-Methods:
Access-Control-Allow-Headers:
Access-Control-Allow-Credentials:

If status is 401:
Token present?
Token expired?
Wrong environment?
Wrong auth scheme?
Cookie missing?

If status is 403:
User role:
Required role or scope:
Resource owner:
Plan or feature restriction:
IP, origin, WAF, or firewall rule:

What changed recently:
New frontend domain / new API domain / deployment / token refresh / auth middleware / proxy / CDN / plugin

Related CodeZips tools for this problem

Use these tools when you want to turn the error into a clear checklist, support note, or next debugging step.

HTTP Status Code Debugging Helper

Use this when you have a specific status code such as 401, 403, 404, 415, 422, 429, 500, 502, or 503 and need a plain-English debugging path.

API Authentication Header Builder and Debug Checklist Generator

Use this when you are unsure whether your bearer token, API key, Basic auth, or custom auth header is formatted correctly.

CORS Preflight Request Explainer and Fix Checklist Generator

Use this when the browser shows a CORS error, an OPTIONS request fails, or the real request never reaches your API route.

API Request Troubleshooting Checklist Generator

Use this when the problem may involve URL, method, auth, headers, body, CORS, timeout, environment, or server logic together.

API Error Response Explainer and Debug Checklist

Use this when the API returns a JSON error body and you need to understand what the status, message, headers, and endpoint clues mean.

API Content-Type and Accept Header Explainer and Fix Checklist Generator

Use this when an auth-looking problem is mixed with body format, JSON parsing, 415 Unsupported Media Type, or Accept header issues.

JSON Error Response Builder for API Projects

Use this when you are building your own API and want clearer 401, 403, validation, rate limit, and server error responses for frontend developers.

Technical references used for this guide

This guide is written as a practical debugging workflow, with status-code and browser-security behavior checked against web platform documentation.

FAQ

What is the difference between 401 and 403?

A 401 usually means the request does not have valid authentication credentials. A 403 usually means the server understood the request and may know who you are, but refuses the action because of permission, role, scope, resource ownership, plan, policy, or security rules.

Is CORS the same as a 401 or 403 error?

No. CORS is a browser security rule based on server response headers. A 401 or 403 is an HTTP response status from the server. They can appear together when the server returns an auth error but the browser is not allowed to expose the response to frontend JavaScript.

Why does my API return 401 in browser but work in Postman?

Postman may be sending a token, cookie, or auth setting that your browser request is not sending. Browser requests can also trigger CORS preflight, omit cookies unless configured, or use a different origin and environment.

Why does adding an Authorization header cause a CORS error?

Cross-origin browser requests with an Authorization header often require a preflight check. The API must allow that header in the CORS response, usually through Access-Control-Allow-Headers. If the server does not allow it, the browser can block the request.

Should an OPTIONS preflight request require authentication?

In most browser API setups, the preflight request should be allowed to return the correct CORS permission headers without requiring the real endpoint authentication first. The actual API request can still require authentication.

Can a CORS error hide the real API error?

Yes. The server might return a useful 401 or 403 JSON error, but if the response does not include the right CORS headers, browser JavaScript may not be allowed to read that body. Check the Network tab to see the underlying status and headers.

What should I check first when I see 401, 403, and CORS together?

Check the browser Network tab first. If the failed request is OPTIONS, debug CORS preflight. If OPTIONS succeeds and the real request returns 401, debug authentication. If the real request returns 403, debug authorization and permissions.

Is mode no-cors a good fix?

Usually no. It can create an opaque response that your frontend code cannot read. For API debugging, it often hides the problem instead of giving your app usable JSON.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top