CORS errors in complex single-page applications often appear as vague browser messages even when the API is online and returning a valid HTTP response.
A request may work in Postman, curl, a mobile application, or a backend service while failing in Chrome, Firefox, Safari, or Edge. This happens because browsers apply the same-origin policy and use Cross-Origin Resource Sharing, or CORS, to decide whether frontend JavaScript may access a cross-origin response.
The problem becomes harder to diagnose when a SPA communicates with several services: an API gateway, authentication provider, CDN, file-storage service, serverless function, analytics endpoint, and internal microservices. Each layer may redirect requests, modify headers, cache responses, or apply a different origin policy.
This guide presents a structured method for identifying the failing layer, understanding preflight requests, configuring credentialed requests safely, and avoiding shortcuts that can create security problems.
Last reviewed: July 16, 2026. Browser behavior, cloud platforms, frameworks, and security guidance may change. Confirm production settings against the current documentation for your browser, gateway, CDN, identity provider, and backend framework.
What CORS Actually Controls
The same-origin policy limits how a document or script from one origin can interact with resources from another origin.
An origin normally consists of three parts:
- The scheme, such as
httporhttps. - The host, such as
app.example.com. - The port, whether explicitly written or implied by the scheme.
| URL | Same Origin as https://app.example.com? | Reason |
|---|---|---|
https://app.example.com/dashboard |
Yes | The path changed, but the scheme, host, and port remain the same |
https://api.example.com |
No | The host is different |
http://app.example.com |
No | The scheme is different |
https://app.example.com:8443 |
No | The port is different |
https://app.example.com?view=reports |
Yes | The query string does not create a new origin |
When browser JavaScript sends a CORS request, the browser includes an Origin request header. The server decides whether that origin should receive permission, and the browser enforces the server’s response.
A basic successful response may include:
Access-Control-Allow-Origin: https://app.example.com
If the header is missing, malformed, duplicated incorrectly, or does not match the requesting origin, frontend JavaScript cannot access the response.
What CORS Does Not Do
Several misunderstandings make CORS troubleshooting more difficult.
CORS Does Not Prove Who the User Is
The Origin header must not be treated as user authentication. Non-browser clients can send their own Origin value, and trusted origins may still contain compromised or vulnerable applications.
Protected APIs must independently validate sessions, access tokens, roles, scopes, tenant membership, and object ownership.
CORS Does Not Always Prevent a Request from Reaching the Server
For some cross-origin requests, the browser may send the request and then refuse to expose the response to JavaScript. This means a state-changing operation could still occur even though the frontend receives a CORS error.
That is one reason CORS must not replace CSRF protection for cookie-authenticated applications.
CORS Does Not Affect Backend-to-Backend Requests in the Same Way
Postman, curl, scheduled jobs, command-line scripts, and backend services are not restricted by browser CORS enforcement. Their success proves that the endpoint is reachable, but it does not prove that its browser policy is correct.
CORS Does Not Repair an Incorrect API URL
An expired domain, incorrect route, DNS failure, TLS error, blocked mixed-content request, service-worker problem, or gateway timeout may look similar from frontend code. Confirm the underlying network result before changing CORS settings.
Why the Browser Often Hides the Real Error
Suppose the API returns:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "expired_access_token"
}
If that response does not also contain the required CORS headers, JavaScript may receive a generic network-style failure instead of the useful JSON message.
CORS headers should therefore be applied consistently to responses that the frontend is intended to read, including controlled validation errors, authentication failures, rate-limit responses, and server errors.
This does not mean that every error must be shared with every origin. The trusted-origin policy still applies.
A Reliable CORS Troubleshooting Workflow
Avoid changing random headers until the console message disappears. Use the following sequence.
Step 1: Record the Exact Frontend Origin
Open the SPA and run this in the browser console:
window.location.origin
Copy the complete result. Commonly confused origins include:
http://localhost:3000
http://localhost:5173
https://staging.example.com
https://app.example.com
https://www.example.com
These are separate origins. An allowlist entry for one does not automatically permit the others.
Step 2: Confirm the Final API URL
Inspect the Network tab instead of relying only on the URL written in source code. Environment variables, reverse proxies, service workers, gateways, CDNs, and redirects may change the final destination.
Verify:
- The scheme is HTTPS in production.
- The hostname is the intended API environment.
- The route and API version are correct.
- No unexpected redirect changes the origin or path.
- The application is not calling an old staging or preview deployment.
Step 3: Determine Whether a Preflight Exists
Filter the Network tab by the API route and look for an OPTIONS request immediately before the application request.
If OPTIONS fails, focus on the preflight first. The browser may never send the intended POST, PUT, PATCH, or DELETE request.
Step 4: Inspect the Final Response Headers
Do not inspect only the origin application’s configuration. Check the response as received by the browser after it passes through every infrastructure layer.
Confirm whether these appear correctly:
Access-Control-Allow-OriginAccess-Control-Allow-Credentials, when requiredAccess-Control-Allow-Methods, on preflight responsesAccess-Control-Allow-Headers, on preflight responsesAccess-Control-Expose-Headers, when JavaScript must read additional response headersVary: Origin, when the allowed origin is selected dynamically
Step 5: Reduce the Request
Temporarily create the smallest request that can reproduce the issue. Remove optional headers, interceptors, credentials, tracing data, and custom content types one at a time.
After the minimal request works, restore each requirement separately. This identifies the exact method, header, credential mode, or infrastructure rule that introduced the failure.
Step 6: Compare Environments
If development works but production fails, compare the actual responses rather than assuming both deployments use the same configuration.
| Environment Detail | Development | Production Risk |
|---|---|---|
| Frontend origin | Localhost and a development port | Real domain may be missing from the allowlist |
| API path | Direct backend URL | Gateway or CDN may change headers and redirects |
| Authentication | Mock token or disabled authentication | Authorization middleware may block OPTIONS |
| Cookies | Local or first-party behavior | Domain, Secure, SameSite, and browser policies may differ |
| Cache | Usually limited or disabled | A CDN may reuse a response created for another origin |
Symptom-to-Cause Diagnostic Table
| Symptom | Likely Cause | First Check |
|---|---|---|
| Works in Postman but not in the browser | Browser CORS enforcement | Inspect the final response headers in DevTools |
| OPTIONS returns 401 or 403 | Authentication middleware is handling preflight incorrectly | Ensure preflight does not depend on user credentials |
| OPTIONS returns 404 or 405 | The route or gateway does not support preflight | Configure an OPTIONS response for the affected resource |
| Fails after adding Authorization | Authorization is not allowed by the preflight policy | Inspect Access-Control-Request-Headers and Access-Control-Allow-Headers |
| Fails after sending JSON | The content type causes a preflight | Allow Content-Type and handle OPTIONS |
| Request succeeds but JavaScript cannot read a response header | The header is not CORS-safelisted or exposed | Add Access-Control-Expose-Headers for the required header |
| Credentialed request rejects wildcard origin | Credentials cannot be shared with Access-Control-Allow-Origin: * | Return the exact approved origin and allow credentials |
| Cookie is missing even though CORS looks correct | Cookie domain, path, SameSite, Secure, expiration, or browser policy | Inspect the Cookies panel and request headers |
| Only some users or origins fail | Incorrect CDN cache variation | Check Vary: Origin and cached response headers |
| API error body is hidden | CORS headers are missing from error responses | Check 401, 403, 404, 429, and 500 responses separately |
Understanding CORS Preflight Correctly
A preflight is an automatic OPTIONS request sent by the browser to ask whether a future cross-origin request is permitted.
It normally contains:
Origin: https://app.example.com
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: authorization, content-type
The server may respond with:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Vary: Origin
A successful preflight response normally uses an HTTP success status such as 200 or 204.
Requests That Commonly Trigger Preflight
- Methods outside the CORS safelist, such as
PUT,PATCH, andDELETE. - The
Authorizationrequest header. - Custom headers such as
X-Request-ID,X-Tenant-ID, orX-Client-Version. - A
Content-Typeoutside the safelisted values, including the commonapplication/jsontype. - Requests explicitly configured to require a preflight by the relevant browser API.
Credentials Do Not Automatically Create a Preflight
Cookies and browser credentials make CORS response rules stricter, but credential mode by itself does not necessarily cause an OPTIONS request.
A credentialed request that otherwise uses a safelisted method and safelisted headers may be sent without a preflight. The response must still permit the exact origin and explicitly allow credentials.
Preflight Requests Do Not Contain the User’s Credentials
The server must not require an access token, session cookie, or HTTP-authentication credential before it can answer the preflight.
The server may still evaluate the requesting origin, method, and requested headers. It should simply avoid routing OPTIONS through middleware that expects an authenticated user.
Important CORS Headers
| Header | Direction | Purpose | Common Mistake |
|---|---|---|---|
Origin |
Request | Identifies the requesting browser origin | Treating it as authentication |
Access-Control-Request-Method |
Preflight request | States the method intended for the actual request | Checking the OPTIONS method instead of the requested method |
Access-Control-Request-Headers |
Preflight request | Lists non-safelisted headers the browser intends to send | Ignoring Authorization or application headers |
Access-Control-Allow-Origin |
Response | Permits one serialized origin or a wildcard where allowed | Returning a comma-separated list or an unvalidated origin |
Access-Control-Allow-Credentials |
Response | Allows sharing when the request uses credential mode include | Using values other than the case-sensitive value true |
Access-Control-Allow-Methods |
Preflight response | Lists methods supported by the CORS policy | Forgetting PATCH, PUT, or DELETE |
Access-Control-Allow-Headers |
Preflight response | Lists request headers allowed by the policy | Forgetting Authorization, Content-Type, or required custom headers |
Access-Control-Expose-Headers |
Actual response | Allows JavaScript to read additional response headers | Assuming every response header is automatically readable |
Access-Control-Max-Age |
Preflight response | Allows the browser to cache preflight permission temporarily | Using a long value before the policy is stable |
Vary: Origin |
Response | Tells caches that the response may differ by requesting origin | Dynamically returning origins without varying the cache key |
Worked Example: SPA Calling an API with a Bearer Token
Consider this deployment:
- SPA:
https://app.example.com - API:
https://api.example.com - Method:
PATCH - Headers:
Authorization,Content-Type, andX-Request-ID
The frontend sends:
fetch("https://api.example.com/v1/profile", {
method: "PATCH",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"X-Request-ID": requestId
},
body: JSON.stringify({
displayName: "Example User"
})
});
Because the request uses PATCH and non-safelisted headers, the browser first sends:
OPTIONS /v1/profile HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: authorization, content-type, x-request-id
A successful preflight response could be:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PATCH
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-ID
Access-Control-Max-Age: 600
Vary: Origin
The browser may then send the PATCH request. Its response still needs an appropriate origin header:
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
{
"updated": true
}
The bearer token is application data in the Authorization header, not a browser cookie. The frontend does not need credentials: "include" merely because it sends a bearer token.
Worked Example: Cookie-Authenticated SPA
Now consider a SPA that uses a protected session cookie:
fetch("https://api.example.com/v1/account", {
method: "GET",
credentials: "include"
});
The API response must use the exact trusted origin and explicitly permit credentials:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
This combination is invalid for a credentialed request:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Even with correct CORS headers, the cookie may still be absent because of its domain, path, expiration, Secure attribute, SameSite policy, or browser privacy restrictions.
CORS and CSRF for Cookie Sessions
A cookie-authenticated API must enforce CSRF protections independently of CORS.
Depending on the architecture, protections may include:
- An appropriate SameSite cookie policy.
- CSRF tokens tied to the user session.
- Validation of
OriginorRefereras an additional defense. - Requiring custom headers for state-changing requests.
- Rejecting unexpected content types.
- Reauthentication for high-risk operations.
A permissive CORS policy can increase risk, but a restrictive CORS policy alone is not a complete CSRF defense.
Safely Building an Origin Allowlist
A production policy should use exact, reviewed origins.
For example:
https://app.example.com
https://admin.example.com
Development origins may be enabled only in the appropriate environment:
http://localhost:3000
http://localhost:5173
Unsafe: Reflecting Every Origin
Access-Control-Allow-Origin: [whatever appeared in Origin]
Reflection is safe only after an exact comparison against a trusted allowlist.
Unsafe: Weak Suffix Checks
A rule such as this may accept attacker-controlled domains:
origin.endsWith("example.com")
It could incorrectly match:
https://attacker-example.com
Exact origin comparison is clearer and safer. When dynamic subdomains are genuinely required, parse the URL and enforce explicit scheme, hostname, port, and ownership rules.
Be Careful with the Null Origin
Sandboxed documents, local files, and other opaque-origin contexts may send:
Origin: null
Do not allow null broadly. Support it only when the use case is intentional, understood, and isolated from sensitive resources.
Response Headers JavaScript Cannot Read Automatically
Even after the response passes the main CORS check, JavaScript can read only CORS-safelisted response headers and headers explicitly exposed by the server.
Suppose the API returns pagination and rate-limit information:
X-RateLimit-Remaining: 47
X-Next-Page: page_8f21
To make them available through response.headers.get(), return:
Access-Control-Expose-Headers: X-RateLimit-Remaining, X-Next-Page
Then the SPA can read them:
const response = await fetch(apiUrl, requestOptions);
const remaining = response.headers.get("X-RateLimit-Remaining");
const nextPage = response.headers.get("X-Next-Page");
Why Vary: Origin Matters
Some APIs dynamically return the requesting origin after checking an allowlist:
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Without Vary: Origin, a browser cache, reverse proxy, or CDN might reuse a response prepared for one Origin when serving a request from another Origin.
This can cause either:
- A trusted application to receive headers created for another origin and fail unexpectedly.
- An incorrect origin-specific response to be served from cache.
If the resource always returns a static Access-Control-Allow-Origin value for every request, its cache strategy may be simpler. If the value changes based on the incoming Origin, vary the response appropriately.
CDNs, Gateways, and Serverless Platforms
In a complex deployment, CORS may be configured in several places:
- Application framework.
- API gateway.
- Load balancer.
- Serverless function response.
- Edge function.
- CDN response-header policy.
- Web server or reverse proxy.
Choose one primary layer to own the policy whenever possible. Multiple layers can produce duplicated or contradictory headers.
Common Infrastructure Failures
- The gateway handles OPTIONS, but the backend handles the actual response with a different origin list.
- The backend returns correct headers, but the CDN removes them.
- The CDN caches one origin-specific response without
Vary: Origin. - A load balancer redirects OPTIONS from HTTP to HTTPS.
- Authentication middleware rejects OPTIONS before CORS middleware runs.
- The application adds CORS only to successful 200 responses.
- The gateway and application both create
Access-Control-Allow-Origin.
Why Redirects Complicate Troubleshooting
Preflight and cross-origin redirects can create confusing results, particularly when authentication systems, trailing-slash normalization, HTTP-to-HTTPS upgrades, or regional gateways are involved.
Prefer returning the preflight response directly from the final API route.
Check for redirects such as:
https://api.example.com/users
↓
https://api.example.com/users/
↓
https://login.example.com/authorize
The SPA should call the intended final API URL, and OPTIONS should not be redirected to an interactive login page.
Frontend Settings That Commonly Cause Problems
The server grants CORS permission, but frontend code determines the shape of the request.
Global Interceptors
An HTTP interceptor may add headers to every request:
X-Requested-With: XMLHttpRequest
X-Client-Version: 4.8.1
X-Tenant-ID: tenant-42
Those headers may cause preflight requests even on endpoints that do not need them. Add headers only where they are required.
Using credentials: include Everywhere
Do not enable credential mode globally unless requests genuinely rely on browser credentials such as cookies or HTTP authentication.
A bearer-token request normally does not need it:
fetch(apiUrl, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
Using mode: no-cors
This is not a normal solution:
fetch(apiUrl, {
mode: "no-cors"
});
A no-CORS request has significant restrictions and normally produces an opaque response that application JavaScript cannot inspect. It does not grant access to protected cross-origin API data.
Mixing HTTP and HTTPS
An HTTPS SPA calling an HTTP API may be blocked as mixed content before CORS can solve anything. Production frontends and APIs should use HTTPS.
Incorrect Environment Variables
Inspect the built application, not only the local environment file. Frontend build systems may embed variables during compilation, leaving production pointed at an old API host.
Development Proxies and the BFF Pattern
Development Proxy
A local development server can proxy:
http://localhost:5173/api
↓
http://localhost:8080/api
From the browser’s perspective, the request appears to use the frontend origin. This is useful for development, but it does not validate the production CORS policy.
Backend-for-Frontend
A backend-for-frontend can place the browser and its application backend on the same public origin:
A BFF can simplify browser CORS, centralize token handling, hide internal services, and apply consistent logging or rate limiting.
It also introduces backend responsibilities, including:
- Session security.
- CSRF protection.
- Authorization enforcement.
- Server-side request forgery prevention.
- Availability and scaling.
- Safe routing to approved upstream services.
Do not create a generic proxy that accepts arbitrary destination URLs from the browser.
Common Dangerous CORS Configurations
| Configuration | Problem | Safer Direction |
|---|---|---|
| Reflect every Origin value | Any website may receive browser permission | Compare against an exact trusted allowlist |
| Wildcard with credentials | Browsers reject the response | Return the exact approved origin |
| Weak subdomain regular expression | Attacker domains or abandoned subdomains may match | Validate parsed hostnames and review subdomain ownership |
| Allow null for private APIs | Opaque-origin documents may gain access | Reject null unless there is a reviewed requirement |
| CORS only on successful responses | The SPA cannot read useful error information | Apply the trusted-origin policy consistently |
| OPTIONS requires login | Preflight cannot provide the required credentials | Evaluate origin, method, and headers before user authentication |
| Multiple infrastructure layers add CORS | Headers may be duplicated or contradictory | Assign one clear owner for the policy |
| Browser extension used as the fix | Only the developer’s browser bypasses enforcement | Correct the API, gateway, proxy, or architecture |
Practical Decision Flow
Production CORS Checklist
When to Involve Backend, DevOps, or Security Teams
Backend Support
Backend involvement is required when:
- The API does not return the correct response headers.
- OPTIONS is rejected by application middleware.
- Error responses are missing the CORS policy.
- The origin allowlist is stored in application configuration.
- Cookies, tokens, authorization, or CSRF behavior must change.
DevOps or Platform Support
Platform involvement is required when:
- A gateway, CDN, proxy, edge function, or load balancer modifies headers.
- The issue appears only in one deployment environment.
- Preflight receives a redirect or infrastructure-generated error.
- Cache behavior differs by origin.
- More than one layer currently owns CORS.
Security Review
Security review is appropriate when:
- The API uses authentication cookies across origins.
- Origins are generated dynamically.
- Many customer or partner domains require access.
- The API exposes administrative, financial, healthcare, or regulated data.
- The team is considering wildcard subdomains or the null origin.
- A BFF or token proxy is being introduced.
- CORS is being relied upon as part of an access-control design.
Conclusion
Resolving CORS errors in complex SPAs requires separating the browser symptom from the underlying network behavior.
Begin with the exact frontend origin and final API URL. Determine whether the failure occurs during preflight or during the actual request. Inspect the final headers received after every gateway, CDN, proxy, and middleware layer.
Use an explicit origin allowlist, support only the methods and headers the application requires, and return Vary: Origin when the response changes by Origin. Credentialed requests require an exact allowed origin and explicit permission.
Most importantly, treat CORS as one browser-response-sharing mechanism. Authentication, authorization, CSRF protection, cookie security, API validation, and infrastructure security must continue to operate independently.
Frequently Asked Questions
Why does the request work in Postman but fail in the browser?
Can CORS be fixed entirely in React, Vue, Angular, or Svelte?
Does using cookies always trigger a preflight?
Why does OPTIONS return 401 even though the user is logged in?
Why can the SPA read the JSON body but not a pagination header?
Is Access-Control-Allow-Origin: * safe?
Official References
- WHATWG Fetch Standard: CORS protocol
- WHATWG Fetch Standard: CORS protocol and credentials
- WHATWG Fetch Standard: CORS preflight fetch
- WHATWG Fetch Standard: CORS and HTTP caches
- WHATWG HTML Standard: Origins
- RFC 9110: HTTP Semantics
- RFC 9111: HTTP Caching
- OWASP Web Security Testing Guide: Testing CORS
- OWASP HTML5 Security Cheat Sheet
- OWASP CSRF Prevention Cheat Sheet

The Linqdev Editorial Team researches, reviews, and publishes practical content about cloud architecture, API security, DevOps workflows, and scalable software systems. Our guides are prepared using official documentation, reliable technical sources, and careful editorial review to provide clear, accurate, and useful information for developers and technology professionals.




