A cross-origin POST from your page fails with a CORS error in the console, yet the record was created on the server anyway. Explain why some cross-origin fetches reach the server before the browser has any permission, and what in your own fetch call decides that the browser asks first instead.
answer
- reading is restricted, sending mostly is not
- forms could always post cross-origin
- JSON content type is not on the safelist
- any header you add trips it
- look for the OPTIONS row above
basics
~20 sCORS controls whether a page may read a cross-origin response, not whether the request is sent. Requests that a plain HTML form could already make are sent immediately and can have side effects; anything beyond that — a method like PUT or DELETE, a Content-Type of application/json, or a custom header — makes the browser check with the server first.
solid answer
~50 sCORS is a read restriction, not a send restriction. A cross-origin request whose method and headers stay within what an ordinary HTML form could already produce is dispatched straight away; the browser only inspects the reply, so a failed check hides the response while the server has already done the work. Anything outside that envelope makes the browser send an `OPTIONS` check to the same URL first and only issue the real request if the server approves — and my own `fetch` call is what pushes it over the line. The usual triggers are a method other than GET, HEAD or POST; a `Content-Type` other than `application/x-www-form-urlencoded`, `multipart/form-data` or `text/plain`; and any header I set myself, such as `Authorization` or `X-Request-Id`. So `Content-Type: application/json` alone is enough. The practical consequence: never assume a CORS error means nothing happened, and design non-idempotent endpoints so a repeated call is safe.
go deeper
Know that a cross-origin request can still reach the server even when the browser blocks the response, and that sending JSON or a custom header changes how the request is made.
Enumerate the envelope precisely — GET, HEAD or POST, safelisted headers only, and one of the three form content types — and name what in your own fetch options pushes a call outside it.
Use the distinction diagnostically: no OPTIONS row means the side effect happened, and design non-idempotent endpoints with idempotency keys rather than assuming a blocked read implies a blocked write.
Weigh the extra round trip against alternatives at scale — same-origin proxying, cookie auth instead of an Authorization header on hot paths — and make sure teams never treat CORS as an authorization control.
## The rule people get backwards The same-origin policy has never stopped a page from *sending* a cross-origin request. An HTML form on any site has always been able to POST to any URL, an `<img>` tag has always been able to GET one, and `<script src>` has always been able to load one. What the browser withholds is the *response*: your script may not read what came back. CORS is built on top of that history. It relaxes the read restriction — it does not tighten the send permission for things that were already possible. So when a cross-origin POST fails with a CORS message, the honest reading is "the browser would not show me the reply", not "the browser refused to make the call". If the endpoint created a record, the record exists. ## The envelope: requests sent immediately A cross-origin request is dispatched with no advance check when everything about it stays inside the envelope of what a form or an image tag could already do: - the method is `GET`, `HEAD` or `POST`; - the only headers your code set are on the safelist — in practice `Accept`, `Accept-Language`, `Content-Language`, `Content-Type` (with restrictions below) and `Range`; - if `Content-Type` is present, its value is one of `application/x-www-form-urlencoded`, `multipart/form-data` or `text/plain` — exactly the three an HTML form can produce. Browser-controlled headers such as `Origin`, `Referer`, `User-Agent` and `Cookie` do not count against you; the restriction is on what *you* add. ```js // Sent immediately. If this endpoint has a side effect, it happened — // the CORS failure only hides the reply. fetch("https://api.other.com/orders", { method: "POST", headers: { "Content-Type": "text/plain" }, body: "id=42", }); ``` ## What pushes a request out of the envelope Step outside it and the browser sends an `OPTIONS` request to the same URL first, describing what you intend to do, and issues the real request only if the server approves. In everyday frontend code, three things do it: 1. **The method.** `PUT`, `PATCH` and `DELETE` are all outside — no HTML form can emit them. 2. **The content type.** `Content-Type: application/json` is the single most common trigger, and it surprises people because JSON feels like the default way to talk to an API. It is outside the three form encodings, so it is outside the envelope. 3. **A header you set yourself.** `Authorization`, `X-Request-Id`, `X-CSRF-Token`, an API-key header, a tracing header injected by a client library — any of them is enough on its own. ```js // Checked in advance: JSON content type *and* a custom header. fetch("https://api.other.com/orders", { method: "POST", headers: { "Content-Type": "application/json", "X-Request-Id": crypto.randomUUID(), }, body: JSON.stringify({ id: 42 }), }); ``` A useful mental shortcut: the advance check exists so that browsers can offer capabilities the web never had before — arbitrary methods, arbitrary headers, JSON bodies — without letting any page fire them at servers that were written before those capabilities existed and might interpret them as authenticated actions. ## Consequences you should be able to name **Side effects are real.** A POST inside the envelope executed. If it charged a card or created an order, retrying after "fixing CORS" produces a second one. Idempotency keys on non-idempotent endpoints are the durable answer, not a CORS setting. **Failure modes differ.** When a request outside the envelope is rejected, the real request never leaves — nothing happened server-side, and your application logs will show only the `OPTIONS`. When a request inside the envelope is rejected, the application logs show the real call, completed. That difference is the fastest way to tell which case you are in. **Latency is real too.** Every advance check is an extra round trip before the request that matters. On a chatty API over a high-latency link this is visible, which is why servers are usually configured to let browsers remember an approval for a while — and why moving the call onto your own origin removes the cost entirely. **Debugging.** In the Network panel, look for an `OPTIONS` row immediately above the real request. Present means the request was outside the envelope; absent means it went straight out and anything it did is done. Some panels hide `OPTIONS` rows by default, so check the filter before concluding there was none.
- You are told to shave a round trip off a hot cross-origin GET. What in your client code could you change?Stop adding anything that leaves the envelope: drop custom headers such as tracing or API-key headers on that particular call, and avoid a non-form `Content-Type`. If the endpoint needs authentication, a cookie is attached by the browser and costs nothing extra, whereas an `Authorization` header always triggers the advance check. The larger win is usually to serve the call from your own origin so no check applies at all.
- How would you tell from the Network panel whether a failed cross-origin POST actually reached your application?Look for an `OPTIONS` entry immediately before the POST. If one is there and was rejected, the POST never left the browser and the server saw only the check. If there is no `OPTIONS` row, the POST was sent straight out and any side effect has already occurred — the server's own access logs will confirm it. Some panels filter `OPTIONS` rows out by default, so verify the filter first.
- Since a cross-origin POST can reach the server regardless, what protects an endpoint from a hostile page?Not CORS — it only governs who may read the reply. The endpoint's own authentication and authorization do, together with the browser's cookie rules that determine whether a session is attached to a cross-site request at all, and with any anti-forgery token the server requires. Treat every request as if it came from an attacker's page, because a non-browser client ignores CORS entirely.
saying these in an interview costs you the question
- Believes CORS prevents the request from being sent
- Assumes a failed cross-origin POST had no side effect
- Thinks only PUT and DELETE are checked in advance
- Says application/json is a safelisted content type
- Treats CORS as protection against cross-site request forgery