401 vs 403 vs CORS: How to Know What Is Actually Broken
Your API request fails, but the error does not clearly tell you what to fix. The browser says CORS. The Network tab shows 401. Postman returns 403. Your React code reports “Failed to fetch.” These errors can appear together, but they describe different layers of the request. This guide shows you how to separate authentication, permission, and browser-access problems before changing the wrong code.
Quick answer: A 401 normally means the API did not receive acceptable authentication credentials. A 403 normally means the server understood the request but refuses the action. A CORS error means the browser did not receive permission to expose a cross-origin response to your frontend code. CORS can hide a real 401 or 403, so inspect the Network tab before deciding what is broken.
Fast Decision Table: 401 vs 403 vs CORS
Start with the failed request shown in the browser Network tab. Do not decide based only on the console message. The console may describe what the browser blocked, while the Network tab reveals what the server actually returned.
| What you see | Plain-English meaning | Likely layer | First check |
|---|---|---|---|
401 Unauthorized |
The API did not accept the request as properly authenticated. | Authentication | Token, API key, cookie, login session, auth scheme, expiration, and environment. |
403 Forbidden |
The server understood the request but refuses the requested action. | Authorization or security policy | User role, scope, ownership, account plan, IP restriction, WAF, CSRF, or server rule. |
| CORS console error | The browser cannot expose the cross-origin response to your JavaScript. | Browser security and server CORS headers | Origin, preflight status, allowed methods, allowed headers, and credentials. |
OPTIONS 401 |
Authentication middleware is rejecting the browser’s preflight check. | CORS plus middleware order | Allow preflight to return CORS headers before applying endpoint authentication. |
OPTIONS 403 |
The origin, requested method, or requested headers may be rejected. | CORS, gateway, firewall, or hosting policy | Compare the exact Origin and requested headers with the server allowlist. |
TypeError: Failed to fetch |
The browser did not give JavaScript a normal readable response. | CORS, network, DNS, TLS, mixed content, blocked request, or extension | Open Network, inspect the failed row, status, headers, redirect, and console details. |
Useful mental model: Authentication asks, “Who are you?” Authorization asks, “Are you allowed to do this?” CORS asks, “May browser code from this origin read this response?”
What a 401 Unauthorized Error Really Means
Despite its name, a 401 response is best understood as an authentication failure. The protected endpoint expected valid credentials, but the credentials were missing, expired, malformed, revoked, sent in the wrong place, or rejected for another authentication reason.
Common causes of a 401 response
- The
Authorizationheader is missing. - The bearer token expired or was revoked.
- The request uses
BearerTOKENinstead ofBearer TOKEN. - The token was created for staging but sent to production.
- The API key is placed in the wrong header or query parameter.
- The browser did not send the login cookie.
- The cookie domain, path, SameSite, or Secure setting prevents delivery.
- A reverse proxy strips the Authorization header.
- The endpoint expects Basic authentication, but the client sends a bearer token.
Correct bearer-token fetch pattern:
const response = await fetch("https://api.example.com/v1/profile", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_TOKEN",
"Accept": "application/json"
}
});
const data = await response.json();
if (!response.ok) {
console.error(response.status, data);
}
If this request returns 401, inspect the actual outgoing header in the Network tab. Do not assume the code you wrote is the request the server received. Interceptors, proxies, environment variables, extensions, middleware, and redirects can change the real request.
Security reminder: Private service API keys should not be embedded in React, Vue, browser JavaScript, or public WordPress HTML. Anything sent from the browser can generally be inspected by the user. Keep private credentials on a backend or server-side WordPress request.
What a 403 Forbidden Error Really Means
A 403 response means the server understood the request but refuses to process it. Authentication may already be valid. Repeating the same login or refreshing the same token will not fix a permission rule, blocked account, missing scope, or resource-ownership problem.
Common causes of a 403 response
- The user is signed in but does not have the required role.
- The token is valid but lacks a required OAuth scope.
- The user is attempting to edit another user’s resource.
- The endpoint is limited to administrators or paid accounts.
- A firewall, CDN, WAF, hosting provider, or security plugin blocks the request.
- The IP address, country, domain, or origin is not allowed.
- A CSRF token, WordPress nonce, or framework permission check failed.
- The account is suspended, disabled, unverified, or outside a permitted organization.
A useful 403 response might look like this:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "forbidden",
"code": "ADMIN_ROLE_REQUIRED",
"message": "You do not have permission to access this resource.",
"request_id": "req_7f93a1"
}
The status tells the client the broad failure category. The machine-readable code explains the application-specific reason. The request ID lets a support team find matching server logs without exposing internal details.
Practical question: If the token is definitely valid, ask whether this identity is permitted to perform this exact method on this exact resource. A user may be allowed to read a project but forbidden from deleting it.
What a CORS Error Really Means
CORS stands for Cross-Origin Resource Sharing. It is a browser mechanism based on HTTP response headers. It allows a server to tell the browser which other origins may read its responses.
An origin includes the scheme, hostname, and port. These URLs are different origins:
http://localhost:3000
http://localhost:5173
https://example.com
https://api.example.com
http://example.com
https://example.com:8443
If your frontend runs at http://localhost:5173 and calls https://api.example.com, the browser treats the request as cross-origin. The API must return permission headers that match the browser request.
Example response headers:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, Accept
CORS is why an API can work in Postman or terminal cURL but fail in browser JavaScript. Postman and cURL can test the raw HTTP endpoint. Browser JavaScript also has to satisfy the browser’s origin rules.
The hidden preflight request
For certain cross-origin requests, the browser sends an OPTIONS request before the real request. This is called a preflight request. It asks whether the origin, method, and headers are allowed.
OPTIONS /v1/projects HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
A successful server response might be:
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 that OPTIONS request returns 401, 403, 404, 405, or a redirect, the browser may stop before sending the real POST. This is why backend logs sometimes show no POST even though the frontend reports that it tried to submit.
How CORS Can Hide a Real 401 or 403
The server can return a real authentication or permission error, but the browser may refuse to expose its response body because the required CORS headers are missing.
Imagine that the API sends:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"code": "TOKEN_EXPIRED",
"message": "Your access token has expired."
}
If the response does not include a valid Access-Control-Allow-Origin header for the frontend origin, JavaScript may not receive that useful JSON body. Your application may only see a generic fetch failure, while the console emphasizes CORS.
| Network result | Likely interpretation | Next action |
|---|---|---|
OPTIONS 401 |
Authentication middleware blocked preflight. | Return CORS permission for OPTIONS before endpoint authentication. |
OPTIONS 403 |
Origin, method, header, gateway, or firewall rule rejected preflight. | Compare the requested origin, method, and headers with the allowlist. |
OPTIONS 204, then POST 401 |
Preflight worked; the real request failed authentication. | Inspect token, cookie, API key, expiration, and auth scheme. |
OPTIONS 204, then POST 403 |
Preflight worked; the real request failed permission checks. | Inspect role, scope, resource ownership, or server security policy. |
GET 401 plus CORS console message |
The server may have returned 401 without CORS headers on error responses. | Fix authentication and ensure error responses include the correct CORS policy. |
Bad fix: Adding Access-Control-Allow-Origin to your frontend request headers does not grant CORS permission. It is a server response header.
The Practical Debugging Order
The fastest approach is to debug the request in layers. This prevents you from rewriting authentication code when the browser is actually failing at preflight.
Write down the exact frontend origin
Include scheme, hostname, and port. Do not write only “localhost.” http://localhost:3000 and http://localhost:5173 are different origins.
Open the Network tab and reproduce the error
Find the failed request. Look for an OPTIONS request immediately before the real GET, POST, PUT, PATCH, or DELETE.
If OPTIONS failed, fix preflight first
Check Access-Control-Allow-Origin, allowed methods, allowed headers, credentials, redirects, middleware order, and gateway rules. Do not debug the JSON body yet because the real request may not have run.
If the real request returned 401, debug authentication
Confirm the token or cookie is present in the actual request. Check expiration, auth scheme, environment, audience, issuer, API-key location, and proxy behavior.
If the real request returned 403, debug authorization
Check role, scope, ownership, plan, organization membership, CSRF or nonce validation, account state, IP policy, WAF, and endpoint-specific permissions.
Compare with a working cURL or Postman request
Compare the final URL, method, Authorization header, cookies, Content-Type, Accept header, request body, and redirect behavior. Compare the actual requests, not screenshots of your source code.
Check server logs using a timestamp or request ID
Server logs tell you whether the request reached the application, which middleware rejected it, and whether the failure came from authentication, authorization, proxy configuration, or application logic.
Practical Request Examples
Bearer token with fetch
async function loadProfile() {
const response = await fetch("https://api.example.com/v1/profile", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_TOKEN",
"Accept": "application/json"
}
});
const data = await response.json();
if (response.status === 401) {
console.error("Authentication failed:", data);
return;
}
if (response.status === 403) {
console.error("Permission denied:", data);
return;
}
if (!response.ok) {
console.error("Unexpected API error:", response.status, data);
return;
}
console.log(data);
}
Cookie-based authentication
When an API uses a login cookie, fetch may need credentials: "include":
const response = await fetch("https://api.example.com/v1/account", {
method: "GET",
credentials: "include",
headers: {
"Accept": "application/json"
}
});
The server also needs compatible credentialed CORS headers:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
A credentialed cross-origin response should use the specific permitted origin rather than a wildcard. Cookie delivery can also depend on the cookie’s domain, path, SameSite setting, Secure setting, and browser privacy controls.
Equivalent cURL test
curl -i "https://api.example.com/v1/profile" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
If cURL succeeds while browser fetch fails, compare browser-specific factors: CORS, OPTIONS preflight, cookies, HTTPS, redirects, and the frontend origin. If both fail with 401, the credential itself is probably the better starting point. If both fail with 403, inspect permissions or server policy.
Common Fixes That Do Not Solve the Real Problem
Using mode: "no-cors"
This usually creates an opaque response that JavaScript cannot read. It may hide the useful error without giving your application usable JSON.
Installing a browser CORS extension
An extension changes your local browser environment. It does not fix production, other users, mobile apps, or the API’s real CORS policy.
Refreshing the token for every 403
A valid token can still lack permission. Re-authentication will not add an administrator role, missing scope, ownership, or paid feature.
Allowing every origin everywhere
Open CORS may be inappropriate for private user data or credentialed APIs. Allow only the origins and resources that actually require browser access.
Protecting OPTIONS with normal endpoint auth
If authentication rejects the permission check itself, the real authenticated request may never be sent by the browser.
Testing only in Postman
Postman verifies the raw HTTP request, but it does not reproduce all browser-origin, credential, and preflight behavior.
WordPress and Custom HTML Note
JavaScript inside a WordPress Custom HTML block runs in the visitor’s browser, so normal browser CORS rules apply. A request may work from PHP on the server but fail from the same WordPress page when called with fetch.
Use browser JavaScript for public APIs that explicitly allow your site’s origin. Use server-side WordPress requests for private API credentials and services that do not support direct browser access.
$response = wp_remote_get(
'https://api.example.com/v1/profile',
array(
'headers' => array(
'Authorization' => 'Bearer SERVER_SIDE_TOKEN',
'Accept' => 'application/json',
),
'timeout' => 20,
)
);
Do not paste a private token into a public Custom HTML block. Server-side PHP can keep the credential out of the page source and browser Network tab.
Copyable 401, 403, and CORS Debugging Note
API authentication and CORS debugging note
Frontend origin:
Example: http://localhost:5173
API URL:
Example: https://api.example.com/v1/projects
Method:
GET / POST / PUT / PATCH / DELETE
Where it works:
Postman:
cURL:
Backend/server:
Browser:
Browser Network tab:
Is there an OPTIONS request?
OPTIONS status:
Real request status:
Was the real request sent?
Redirect present?
Authentication:
Bearer token / API key / cookie / Basic auth / OAuth / none
Authorization header present:
Cookie present:
Token expired:
Correct environment:
Required user role:
Required scope:
Resource owner:
CORS response headers:
Access-Control-Allow-Origin:
Access-Control-Allow-Methods:
Access-Control-Allow-Headers:
Access-Control-Allow-Credentials:
Exact browser error:
Paste the error without private credentials.
Server logs:
Request ID:
Timestamp:
Middleware or rule that rejected the request:
Likely category:
401 authentication
403 permission
CORS preflight
CORS response header
Cookie or credentials
Proxy, WAF, or hosting policy
Technical References
Frequently Asked Questions
What is the simplest difference between 401 and 403?
A 401 generally means the request does not have acceptable authentication credentials. A 403 generally means the server understands the request but refuses the action because of permission, scope, ownership, account state, or another policy.
Is CORS an authentication error?
No. CORS is a browser access-control mechanism. However, an authentication request can trigger preflight, and a server can return 401 without the CORS headers needed for browser JavaScript to read the response.
Why does my API work in Postman but fail in the browser?
Postman tests the HTTP endpoint without enforcing browser CORS rules in the same way. Browser requests may require preflight, matching origin headers, credential settings, and secure cookie behavior.
Why does adding an Authorization header trigger CORS?
A cross-origin browser request using Authorization commonly requires a preflight request. The server must allow that header through its CORS response, including an appropriate Access-Control-Allow-Headers value.
Should an OPTIONS request require a bearer token?
The preflight request normally needs to receive the CORS permission response before the browser sends the real authenticated request. If regular authentication middleware blocks OPTIONS first, the real request may never run.
Can CORS hide an expired-token error?
Yes. The server may return a 401 JSON response explaining that the token expired, but the browser may refuse to expose that response body if the required CORS headers are missing.
Should I use no-cors mode to fix a failed API request?
Usually not. It commonly produces an opaque response that your frontend cannot inspect or parse as normal API JSON. Fix the server policy or use a server-side request instead.
What should I check first?
Open the browser Network tab. Determine whether the failed request is OPTIONS or the real API method. Fix failed preflight first, 401 authentication second, and 403 permission rules after authentication is confirmed.
Final Takeaway
Do not treat 401, 403, and CORS as interchangeable errors. A 401 points toward authentication. A 403 points toward permission or policy. CORS decides whether browser JavaScript may read a cross-origin response. Start with the Network tab, identify whether OPTIONS or the real request failed, and debug the layers in order. That approach is faster, safer, and far more reliable than changing random headers until the message disappears.