← Back to writing

Article · web-security / cors / http

Why a CORS error does not mean the request was never sent

A practical explanation of the difference between a rejected preflight and blocked access to the response of a request that was sent.

When a CORS error appears in the browser console, the first explanation that comes to mind is that the request never reached the server. Yet the same error can also appear after the request has been sent and the server has responded. The browser withholding a response from the page's JavaScript and stopping a request from being sent happen at different stages. When debugging, we first need to identify which stage the request reached.

This distinction is especially important for operations that change data. An error on screen does not, on its own, show that the operation failed. Checking the application's state and server logs before retrying the same operation helps prevent unnecessary repeats. The domain names and scenarios in this article are illustrative educational examples; they do not describe the results of tests performed on a real system.

First, distinguish origin from site

For HTTP and HTTPS URLs, origins are compared using the scheme, hostname, and effective port. Changing the path does not change the origin. For example, /profil and /ayarlar on the same server have the same origin. A change in subdomain or port makes the relationship cross-origin. The details of this model are defined in the WHATWG HTML standard.

Site comparisons work differently: for ordinary domain-name examples, the scheme and registrable domain are taken into account. This means https://app.example.test and https://api.example.test can be same-site even though they are cross-origin. https://external.other.test is a different site. Switching from HTTP to HTTPS also changes the result of a schemeful site comparison. It is important to preserve this distinction when interpreting cookie behavior. The web.dev explanation compares the two concepts with examples.

Sending the request versus reading the response

The same-origin policy does not prohibit all interactions between different origins in the same way. Form submissions, some navigations, and resource embedding may be possible, while reading another origin's data through a script may be restricted. MDN's same-origin policy guide explains this distinction in terms of writes, embedding, and reads.

CORS lets a server indicate which origins' scripts may receive a response. Consider a cross-origin GET request that meets the conditions for being sent without a preflight. The request is sent and the server produces a response, but if that response lacks the necessary CORS permission, the calling JavaScript cannot read it. A console error can appear at the end of this sequence. The MDN CORS guide illustrates the flow of these requests.

This is why seeing an HTTP status code in the Network panel is not the same observation as application code receiving a usable Response. A 200 on its own also does not tell us that a particular business rule was successfully fulfilled. Application records or controlled test data showing the operation's outcome must be examined separately.

What happens when a preflight is rejected?

For some cross-origin requests, the browser checks permission before sending the actual request. This is called a preflight. For example, the PATCH method and certain headers can result in an OPTIONS request like this:

http
OPTIONS /v1/profile HTTP/1.1
Host: api.example.test
Origin: https://app.example.test
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: content-type,x-lab-request

If the server's response does not provide the necessary permissions for the requested origin, method, and headers, the browser does not send the actual PATCH request in this flow. The fact that OPTIONS was sent does not show that PATCH was sent as well. When the preflight succeeds, the response to the actual request must also pass the required CORS check. The Fetch standard defines these as separate stages.

There are two different error scenarios here:

  • The preflight failed; the actual request was not sent.
  • The actual request was sent; its response could not be shared with the script.

Not every cross-origin request requires a preflight. Falling into the category commonly called a “simple request” does not mean a request is harmless or authorized; it only means that it meets the relevant browser conditions. The browser may also reuse a previously granted preflight permission for as long as it remains valid. It is therefore wrong to expect a new OPTIONS request on every attempt. MDN CORS guide

Cookies and credentials are a separate layer of checks

Using credentials: "include" in a fetch call requests that eligible credentials be included in the request. This does not guarantee that a cookie will be sent; cookie scope, SameSite rules, and the browser's relevant policies also apply. A same-site subdomain call and a cross-site call should not be evaluated using the same assumption.

Sharing a credentialed CORS response requires the appropriate explicit origin in Access-Control-Allow-Origin and Access-Control-Allow-Credentials: true. The wildcard origin * cannot be used in this case. Arbitrarily reflecting the allowed origin value does not constitute a secure allowlist either. Fetch section on CORS and credentials

These headers do not replace access control in the application. The user's identity, permission to access the requested resource, and the conditions for the operation must be checked on the server. Similarly, an origin value appearing in a response does not, on its own, prove that sensitive data can be read; the actual sharing behavior and the data's context must be evaluated together.

A CORS error does not settle a CSRF assessment

In a CSRF assessment, the question is whether an unwanted operation can be caused using the user's automatically attached credentials. The party triggering the operation does not need to be able to read the response in every scenario. Seeing a CORS error or looking at a preflight result is therefore not enough, on its own, to conclude that “CSRF is present” or “CSRF is absent.”

Server-side CSRF checks, accepted content types, origin validation, and cookie policy must be considered together in the context of the application's flow. The OWASP CSRF prevention guide explains how these controls can complement one another. This article covers the CORS flow; other browser security policies are not examined separately here.

What evidence should you examine when debugging?

In your own application or an authorized lab, investigate in small, observable steps:

  1. Record the origin where the call starts, the target URL, the method, and any headers added.
  2. Locate OPTIONS and the actual request separately in the Network panel. Make sure the panel's filters are not hiding the relevant entry.
  3. Examine the request and response headers on the relevant entry; do not confuse the headers of an OPTIONS response with those of the actual response.
  4. Record the console error, JavaScript's ability to read the result, and the server-side operation's outcome as separate observations.
  5. For operations that change data, verify the outcome before retrying. Do not share secrets such as tokens and cookies in investigation records.

The Chrome DevTools reference is a useful starting point for the Network panel's request, header, and initiator-chain fields. A proxy's Repeater tool or an HTTP client outside the browser may display the response; that does not prove that the browser will expose the same response to the page's script. A claim about browser behavior must be verified in the browser context.

Finally, mode: "no-cors" is not a solution for obtaining a readable API response. In this mode, a cross-origin response may be opaque; the script cannot access the status code, headers, and body in the normal way. The MDN Request.mode documentation explains this limitation. Sound diagnosis starts by identifying which stage the request reached, rather than guessing from the error's name.

← Back to writingmuhammet.saridogan.work@gmail.com