XHR and CORS Explained: What Cross-Origin Resource Sharing Means for Web Development
If you have ever seen a browser block a request with a CORS error, the problem is usually not the API itself. It is the browser enforcing rules around XMLHttpRequest (XHR), the same-origin policy, and Cross-Origin Resource Sharing (CORS).
XHR is the older browser API used to send asynchronous HTTP requests from JavaScript. CORS is the browser mechanism that lets a server explicitly say which outside origins may read its responses. Together, they shape how modern web apps talk to APIs, microservices, CDNs, authentication services, and third-party integrations.
This matters because most real applications are not served from a single domain anymore. Your frontend may run on one host, your API on another, fonts from a CDN, and authentication on a separate subdomain. The result is a steady stream of cross-origin requests that either work cleanly or fail loudly, depending on how the server is configured.
In this article, you will get a practical explanation of XHR, the same-origin policy, CORS headers, preflight requests, credentials, and the most common error patterns. You will also see how to configure CORS safely and how to debug the failure messages browsers throw at you. For reference, the browser security model behind this behavior is documented in the MDN Same-Origin Policy and the request/response mechanics are covered in the MDN XMLHttpRequest docs.
What XHR Is and How It Works
XMLHttpRequest is a JavaScript API that lets a web page send HTTP requests without reloading the page. The name is old, but the API is still common in legacy codebases, browser extensions, and libraries that abstract requests for you.
Typical XHR use cases include fetching JSON data from an API, submitting a form asynchronously, polling for updates, and loading user-specific content after the page renders. In practical terms, XHR gives the browser a way to ask for data in the background and update the page when the response comes back.
Why XHR changed how web apps feel
Before asynchronous requests became standard, a form submission or page action often forced a full page reload. That made sites feel slow and brittle. XHR made it possible to update part of a page without discarding the whole document, which is why it became foundational for AJAX-style applications.
Today, many developers prefer the fetch API for new work because the syntax is cleaner and the promise-based model fits modern JavaScript better. Even so, CORS behavior applies to both fetch and XHR, which is why understanding XHR still helps you understand cross-origin browser rules.
XHR is not CORS
It is easy to mix up the API with the policy, but they are different. XHR is the request tool. CORS is the permission model the browser uses when the request crosses an origin boundary.
That distinction matters when troubleshooting. If the request is sent but the browser refuses to let JavaScript read the response, the issue is usually CORS configuration, not whether XHR itself is broken. The browser is deciding whether the response is safe to expose to client-side code.
Browser security is designed to limit what one site can read from another site, even if both are open in the same browser. That is the core reason CORS exists at all.
For a deeper vendor-neutral explanation of browser request behavior, see MDN: Using XMLHttpRequest and MDN: CORS.
The Same-Origin Policy and Why It Exists
The same-origin policy is a browser security rule that compares three parts of a URL: scheme, host, and port. If any one of those is different, the browser treats the request as cross-origin.
Examples help here. https://app.example.com and https://api.example.com are different origins because the host differs. https://app.example.com and http://app.example.com are different origins because the scheme differs. https://app.example.com and https://app.example.com:8443 are different origins because the port differs.
What the browser allows and blocks
By default, browsers allow a page to make cross-origin requests in many cases, but they do not automatically allow JavaScript to read the response. That is the key point. A request can leave the browser and still be blocked from the page if the response does not satisfy CORS rules.
This policy protects users from a malicious site trying to read private data from another site where the user is logged in. Without it, a page could potentially probe session data, steal profile information, or abuse browser access to sensitive resources.
Real-world origin examples
- Same origin:
https://shop.example.comcallinghttps://shop.example.com/api/orders - Different subdomain:
https://shop.example.comcallinghttps://api.example.com/orders - Different port:
https://localhost:3000callinghttps://localhost:8080/api - Different protocol:
http://localhost:3000callinghttps://localhost:3000/api
Note
The same-origin policy is a browser rule, not a server rule. Your backend may accept the request, but the browser can still prevent JavaScript from reading the response if CORS is not configured correctly.
For more detail on browser-origin behavior, review the MDN Same-Origin Policy and the W3C security overview.
Why CORS Was Created
Web applications needed a controlled way to talk across origins without tearing down browser security. That need is why CORS was created. It gives servers a way to say, “This browser origin is allowed to read my response.”
That permission model is important in modern architectures. A frontend app may call a public API, a single-page application may depend on a separate authentication domain, and a website may load fonts or analytics from other hosts. All of those are cross-origin patterns that are normal and often necessary.
Common scenarios where CORS is necessary
- API consumption: a React, Angular, or Vue frontend calls a JSON API on another subdomain
- Microservices: one service exposes data to a browser-based portal while running on a separate host
- CDN-hosted assets: fonts, scripts, or images are served from a different domain
- Headless CMS: content is delivered from a CMS API to a frontend application on another origin
- Third-party integrations: payment, identity, or mapping services expose browser-facing endpoints
CORS does not replace authentication. It does not authorize a user, validate a token, or grant application-level access. It simply lets the browser know whether a cross-origin response may be exposed to JavaScript. The server still needs proper authentication, authorization, logging, and input validation.
That is why CORS is often misunderstood. It is a browser-enforced access control layer, not a universal “open this up” switch. Good configuration is selective, not permissive. The best configuration in production usually allows only known origins, known methods, and known headers.
For official background on browser security and origin handling, consult MDN CORS and the browser security guidance in MDN Web Security.
How a CORS Request Flows in the Browser
A cross-origin request starts like any other JavaScript request. XHR or fetch sends the request, the server processes it, and the browser evaluates the response before exposing it to the page. That last step is where CORS matters.
The browser checks the response headers to see whether the server has explicitly allowed the requesting origin. If the headers match the browser’s expectations, the JavaScript code gets access to the response body and some response metadata. If they do not match, the browser blocks access even if the server returned a perfectly valid HTTP 200 response.
Simple requests versus preflighted requests
Some requests are considered simple requests and can go straight to the server. Others trigger a preflight, which is an OPTIONS request the browser sends first to ask what is allowed. Preflight exists because some methods and headers are more likely to affect security or trigger side effects.
Examples of requests that often trigger preflight include PUT and DELETE requests, custom headers such as Authorization, and requests that use content types outside the “simple” set. The browser is essentially asking, “Before I send the real request, do you permit this method, these headers, and this origin?”
- The browser creates the XHR or fetch request.
- If needed, it sends a preflight OPTIONS request first.
- The server replies with CORS headers describing what is allowed.
- If the response is acceptable, the browser sends the main request.
- The server returns the actual data.
- The browser exposes the response to JavaScript only if the CORS check passes.
One important detail: the request may still reach the server even if the browser later blocks the response from JavaScript. That means server logs can show successful processing while the frontend still reports a CORS failure. This is a common source of confusion during troubleshooting.
CORS failure does not always mean the request never happened. It often means the browser refused to expose the response to client-side code after the server already handled it.
Authoritative references for the flow include MDN CORS and the HTTP semantics in MDN OPTIONS.
CORS Headers You Need to Know
CORS is controlled by response headers. If you understand the headers, you can usually predict whether a request will succeed before you even open the browser console. The main headers are Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Allow-Credentials.
Access-Control-Allow-Origin
This is the most visible CORS header. It tells the browser which origin may read the response. A value like Access-Control-Allow-Origin: https://app.example.com allows only that specific site. A wildcard * allows any origin, but it is not suitable for every case.
Use a wildcard only when the data is public and does not involve credentials. If cookies, session tokens, or sensitive data are involved, a specific origin is the safer choice. Many developers also support a controlled origin list rather than hardcoding a single domain.
Access-Control-Allow-Methods and Access-Control-Allow-Headers
Access-Control-Allow-Methods tells the browser which HTTP methods are acceptable for cross-origin requests. If the client wants to send PUT, PATCH, or DELETE, the server should list those methods explicitly. This keeps behavior aligned with the API design.
Access-Control-Allow-Headers is equally important when the frontend needs custom headers such as Authorization, X-Request-Id, or X-CSRF-Token. If the browser asks to send a header that the server did not permit, the request can fail at the preflight stage.
Access-Control-Allow-Credentials and expose headers
Access-Control-Allow-Credentials matters when the browser should send cookies, client certificates, or HTTP authentication information. If this is set to true, the server is allowing credentialed cross-origin requests, which raises the security bar. In this mode, the origin cannot be a wildcard.
Another useful header is Access-Control-Expose-Headers. By default, JavaScript can only read a limited set of “simple” response headers. If your application needs to read a custom header like X-RateLimit-Remaining or X-Trace-Id, the server must expose it explicitly.
| Header | What it controls |
| Access-Control-Allow-Origin | Which origin may read the response |
| Access-Control-Allow-Methods | Which HTTP methods are allowed |
| Access-Control-Allow-Headers | Which request headers are allowed |
| Access-Control-Allow-Credentials | Whether credentials may be included |
For official browser behavior and header semantics, see MDN Access-Control-Allow-Origin and MDN Access-Control-Allow-Credentials.
Preflight Requests and When They Happen
A preflight request is an OPTIONS request the browser sends before the real request when the cross-origin call is not simple. It is the browser’s way of asking for permission before it sends something potentially sensitive or nonstandard.
Preflight usually happens when the request uses methods like PUT, PATCH, or DELETE, when it sends custom headers, or when it uses a content type that falls outside the browser’s simple request rules. Authorization headers are one of the most common triggers in real applications.
What the browser asks during preflight
The browser includes headers that describe the real request it wants to send. For example, it may indicate the intended method, the request origin, and the headers it plans to use. The server must then answer with matching permissions.
If the server omits a required method or header, the browser stops the process. The main request never leaves the browser. That is why preflight failures often look like “nothing happened” from the frontend even though the issue is actually a permissions mismatch.
Examples of common preflight cases
- PUT to a REST API: updating a profile record from a browser app
- DELETE request: removing a resource from a user dashboard
- Authorization header: token-based API access from a single-page app
- Custom JSON headers: adding X-Request-Id for traceability
- Non-simple content types: specialized API payload handling
Pro Tip
When debugging a preflight issue, inspect the OPTIONS request first. If the browser blocks the real request, the preflight response is often where the mistake is hiding.
For the request method rules and browser handling, review MDN simple requests and the HTTP method reference at MDN OPTIONS.
Credentials, Cookies, and Authentication
Credentialed cross-origin requests are where many teams get burned. Browsers do not treat cookies, session data, and authentication headers casually, because those mechanisms can expose private user data if misconfigured.
By default, cross-origin requests may omit credentials unless the client explicitly includes them and the server explicitly permits them. When credentials are involved, the server must set Access-Control-Allow-Credentials: true and must use a specific origin, not *.
How sessions and tokens behave
Traditional web apps often rely on cookies and server-side sessions. Single-page applications frequently use bearer tokens or API gateways. In both cases, CORS must line up with the authentication model. If the frontend sends cookies, the browser will only expose the response if the server’s CORS rules support credentialed access.
That is why you often see separate setup for login, refresh tokens, and API calls. A permissive CORS policy on a public endpoint may be fine, while a private endpoint should be tightly locked to the application’s known origin.
Security concerns with credentialed requests
- Never combine wildcard origin with credentials.
- Do not trust user-controlled origin values.
- Keep session cookies scoped and secure.
- Use CSRF protections where browser cookies are used for authentication.
- Limit allowed origins to exact production domains whenever possible.
This is a place where CORS and application security meet. A browser may allow a request because CORS is configured, but the backend still has to validate the user, the token, and the requested action. For broader context on credential handling and web security controls, see OWASP CSRF guidance and browser security notes in MDN CORS.
Common CORS Errors and How to Troubleshoot Them
The classic browser message often says the request was blocked by the CORS policy. That does not automatically mean the backend is down. It usually means the browser did not like one of the headers, the origin did not match, or the preflight response was incomplete.
Frequent causes include a mismatch between the requesting origin and the value returned in Access-Control-Allow-Origin, missing OPTIONS handling on the server, and invalid header values. Redirects can also cause problems, especially when a request begins on HTTPS and gets sent somewhere else unexpectedly.
What to check first
- Open browser developer tools. Go to the Network tab and inspect the OPTIONS request and the main request.
- Check the response headers. Confirm the origin, methods, headers, and credential settings match what the client is sending.
- Look for redirects. A redirect can break CORS even when the final destination is valid.
- Verify protocol and port. HTTP versus HTTPS or a different local development port can change the origin.
- Check backend logs. The server may be receiving the request even if the browser blocks the response.
Common development mistakes
- access-control-allow-origin htaccess: setting a header in Apache but forgetting to handle preflight correctly
- access-control-allow-origin nginx: adding the header at the wrong level so OPTIONS responses miss it
- access-control-allow-methods header: listing GET and POST but forgetting PUT or DELETE
- allow cross origin header: using a vague rule that works in test but is too open for production
Warning
Do not “fix” a CORS error by opening the policy to every origin in production. That may hide the symptom while creating a security problem that is harder to detect later.
Vendor and standards references for debugging and server behavior include MDN CORS and official server documentation from your platform of choice, such as NGINX headers module.
How Developers Configure CORS on the Server
CORS is usually configured on the backend, in middleware, a reverse proxy, or an API gateway. The basic process is simple: identify the trusted frontend origins, decide which methods and headers are allowed, and return the right response headers consistently.
The safest implementations are explicit. They do not trust arbitrary input for origin values, and they do not use a blanket policy when the API contains private data. The goal is to support the application’s actual browser traffic, not every possible browser on the internet.
Where CORS is commonly configured
- Application middleware: framework-level settings in Node.js, Python, .NET, or Java apps
- Reverse proxies: NGINX or Apache adding or forwarding response headers
- API gateways: centralized control for multiple services
- Load balancers or edge layers: header management at the perimeter
How to configure it safely
Start with the minimum viable policy. Allow only the exact frontend origin used in production, then add staging or development origins separately. Match allowed methods to the endpoints you actually expose. If your app only needs GET and POST, do not advertise PUT or DELETE just because the framework allows it.
Also make sure credential settings are aligned with the authentication model. If the app uses cookies, confirm that the server and the browser both support credentialed requests. If the app uses bearer tokens, validate whether preflight is triggered and whether the Authorization header is allowed.
The safest CORS configuration is boring. It should be predictable, explicit, and narrow enough that you can explain every allowed origin and method in one sentence.
Official references for backend configuration patterns are available in the documentation for your platform, such as Microsoft Learn, NGINX documentation, and framework-specific vendor documentation. For browser validation, always verify the final response in the Network tab, not just in server logs.
Best Practices for Secure and Reliable CORS
Good CORS configuration is part security, part reliability. The same settings that keep hostile origins out also help your team avoid flaky browser behavior across development, staging, and production environments.
The first rule is to use the most specific origin list possible. If your app only runs on one production domain, allow that one domain and nothing else. If you need multiple origins, define them explicitly. Do not assume a wildcard is harmless just because the endpoint is “only an API.”
Practical best practices
- Use exact origins. Prefer specific scheme, host, and port values.
- Allow only needed methods. Keep GET, POST, PUT, PATCH, and DELETE limited to actual use cases.
- Allow only needed headers. Keep custom headers intentional.
- Use credentials carefully. Combine cookies and CORS only when the user flow requires it.
- Separate environments. Keep dev, staging, and production CORS settings distinct.
- Test after each change. Verify both allowed and denied cases.
CORS is not a substitute for authentication, authorization, CSRF protection, or secure session management. It is one control in a larger stack. If the endpoint returns sensitive data, the backend still needs access control checks, rate limiting, logging, and input validation.
Security guidance from OWASP CORS guidance is especially useful because it focuses on real implementation mistakes, not just theory. For browser-side enforcement, the MDN CORS reference remains the clearest starting point.
Key Takeaway
Configure CORS to match your real application traffic, not your wish list. If the frontend does not need it, do not allow it.
CORS in Modern Front-End Development
Single-page applications, microservices, and headless CMS deployments all increase the chance that browser requests will cross origins. That is normal now. The challenge is to design the architecture so CORS supports the app instead of fighting it.
Local development is where many teams first run into CORS pain. The frontend runs on localhost:3000, the API on localhost:8080, and the browser treats that as cross-origin because the port differs. That setup is not wrong; it just requires deliberate configuration.
Why developers hit CORS so often
- Frontend and backend on different domains: common in separate deployment pipelines
- Microservices behind an API gateway: browser traffic comes through one edge but reaches multiple services
- Third-party integrations: external APIs may need to be called from browser code
- Local development mismatches: different ports, schemes, or hostnames trigger cross-origin rules
Tools and habits that help
Browser developer tools are the first place to look, because they show the exact request headers, response headers, and preflight status. Local proxy setups can also help during development by making the frontend and API appear same-origin. That can reduce friction, but it should not replace correct server-side CORS configuration.
It is also useful to compare browser behavior with other clients. For example, an API may work in a direct HTTP client and still fail in the browser because the browser applies CORS while the other client does not. That is a useful reminder that CORS is not an API bug; it is a browser security boundary.
For broader architectural context, see guidance from MDN CORS and browser security documentation from the web.dev Fetch API guide.
Conclusion
XHR and CORS are central to how browsers handle cross-origin web communication. XHR is the browser API that sends asynchronous requests, while CORS is the browser-enforced permission model that decides whether JavaScript may read the response.
The same-origin policy exists to protect users. CORS adds a controlled exception to that policy when a server explicitly trusts a requesting origin. Preflight requests, response headers, and credential rules all work together to make that trust visible and enforceable.
The practical lesson is straightforward: configure CORS narrowly, match it to the application’s actual needs, and use browser developer tools to troubleshoot failures one header at a time. A CORS error is not a dead end. It is usually a clue that points directly to the missing method, header, origin, or credentials setting.
For IT teams building modern browser-based applications, understanding XHR and CORS is not optional. It is part of building systems that are secure, debuggable, and reliable across environments. If you are standardizing your frontend-to-API traffic, use official browser documentation, verify your server headers, and keep your CORS policy as small as possible.
To go further, review the official references from MDN, W3C CORS specification, and the security guidance on OWASP. Those sources are the best starting point when you need a policy that works in the browser and holds up in production.