cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

CORS & cross-origin requests

Learn how the same-origin policy, preflight requests, CORS response headers, and credentials decide whether browser JavaScript may read a cross-origin response.

By the end, you can
  • 01
    Compare originsDecide whether two URLs share a scheme, host, and port.
  • 02
    Predict preflightsSpot which requests need an OPTIONS check before the real request.
  • 03
    Read CORS headersExplain why a response is exposed, blocked, opaque, or unsafe with credentials.

The browser as gatekeeper

A web page can ask for many resources: JSON, images, fonts, scripts, and documents. But when JavaScript tries to read a response from another origin, the browser checks a permission system called CORS: Cross-Origin Resource Sharing.

CORS is not a JavaScript syntax feature and it is not a magic header you add to fetch. It is a conversation between the browser and the server. The browser asks, “May this page read that response?” The server answers with response headers such as Access-Control-Allow-Origin. If the answer is missing or mismatched, the browser refuses to hand the response to your code.

Real-life analogyThe apartment mailroom

Imagine an apartment building where the mailroom lets residents open mail addressed to their own apartment, but not their neighbor’s. CORS is the neighbor leaving written permission: “Apartment 4A may read my mail.” Without that note, the mailroom clerk keeps the envelope closed.

In real life: Apartment 4A mail
In JavaScript: Responses from your own origin
In real life: Apartment 4B mail
In JavaScript: Responses from another origin
In real life: Written permission at the desk
In JavaScript: Access-Control-Allow-Origin
In real life: The mailroom clerk
In JavaScript: The browser enforcing the rule

Where the analogy stops: Real mailrooms stop delivery too. CORS mostly controls whether JavaScript may read the response; simple requests can still reach the server.

The practical debugging question

When a request fails in the browser, ask four things in order: are the origins different, is the request simple or preflighted, do the response headers allow it, and are credentials involved?

The same-origin policy

INTERACTIVE

An origin is exactly three pieces: scheme, host, and port. The origin of https://example.com:443/products?q=shoes is https://example.com. The path and query string are not part of it.

The same-origin policy is a browser security boundary. JavaScript from one origin can read its own origin freely. To read a different origin, it needs CORS permission. This protects private data, such as an intranet page or a signed-in account page, from being silently read by any random site you visit.

Origin checker
Compare originsPop out in the code editor (opens in a new tab)JavaScript
function sameOrigin(a, b) {  const left = new URL(a);  const right = new URL(b);  return left.origin === right.origin;}
Resultcross
A originhttps://shop.example
B originhttp://shop.example
DecisionCross-origin
Try it yourself

Cross-origin: at least one of scheme, host, or effective port differs.

Try default ports, subdomains, localhost, and IP addresses. The code uses the browser-compatible URL parser.

Notice the tricky cases: http and https are different schemes; www.example.com and example.com are different hosts; different ports are different origins; and localhost is not the same host text as 127.0.0.1.

A real CORS failure, without leaving this site

REAL BROWSER

This lesson is a static site, so we will not call an outside API. We can still make a real cross-origin request safely. A sandboxed iframe with sandbox="allow-scripts" and no allow-same-origin gets an opaque origin. Browsers expose that as null. From inside that frame, this page’s own origin is cross-origin.

Real cross-origin fetch from an opaque iframe
Frame fetch codePop out in the code editor (opens in a new tab)JavaScript
// Inside a sandboxed iframe with an opaque origin:const target = "https://this-site.example"; async function corsMode() {  try {    const response = await fetch(target + "/javascript");    return response.type + " " + response.status;  } catch (error) {    return error.name + ": " + error.message;  }} async function noCorsMode() {  const response = await fetch(target + "/javascript", { mode: "no-cors" });  return response.type + " " + response.status;}
Sandboxed framemounting

Click a button to run real browser fetch code.

Try it yourself

The iframe is sandboxed without allow-same-origin, so its origin is opaque (null). Fetching this site is cross-origin even though everything stays on your machine.

Messages are reported by real browser code inside the iframe. CORS TypeError text is browser-specific, so the lesson renders exactly what your browser reports.

The first button uses normal fetch mode. The static site does not send an Access-Control-Allow-Origin header for the frame’s opaque origin, so the promise rejects with a TypeError. The exact message differs by browser. The second button uses mode: "no-cors". That does not fix CORS; it gives JavaScript an opaque response with unreadable details. The third button fetches a data: URL so you can compare a readable local result.

Simple vs preflighted requests

STEP THROUGH

Some cross-origin requests are called simple. The name is historical; it does not mean “safe.” A simple request uses GET, HEAD, or POST, uses only CORS-safelisted request headers, and if it sets Content-Type, the value is one of application/x-www-form-urlencoded, multipart/form-data, or text/plain.

A request outside those limits is preflighted. Before the real request, the browser sends an automatic OPTIONS request with headers like Access-Control-Request-Method and Access-Control-Request-Headers. If the server says yes, the browser sends the real request.

Real-life analogyCalling ahead for an unusual package

If you deliver a postcard, the front desk may just pass it along. If you bring a locked crate with special paperwork, you call first: “I am bringing a PUT with a custom header. Is that OK?” The answer can be cached for a while.

In real life: A normal postcard
In JavaScript: A simple GET or form-like POST
In real life: A PUT package with special paperwork
In JavaScript: A non-simple request
In real life: Calling the front desk first
In JavaScript: The preflight OPTIONS request
In real life: Permission kept on file
In JavaScript: Access-Control-Max-Age

Where the analogy stops: The analogy hides timing details. Browsers decide exactly which headers are safelisted and when a preflight cache entry may be reused.

Preflight or not?
  • GET without custom headers
  • POST with text/plain
  • POST with application/json
  • PUT
  • GET with Authorization header
  • POST with FormData
Try it yourself
0 of 6 correct

Sort each request by whether a cross-origin browser request needs a preflight.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Step through a preflighted PUT
Step 0 of 5Ready
Your turn: follow the blue line

Predict whether this browser will expose the response. Then step through the browser-style checks.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  origin: "https://app.example",  method: "PUT",  headers: {    "Content-Type": "application/json",    "X-Request-Id": "abc123"  },  credentials: "include"}; const response = {  "Access-Control-Allow-Origin": "https://app.example",  "Access-Control-Allow-Methods": "GET, POST, PUT",  "Access-Control-Allow-Headers": "Content-Type, X-Request-Id",  "Access-Control-Allow-Credentials": "true",  "Vary": "Origin"}; console.log(decideCors(request, response).verdict);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Server response to test
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

CORS headers

SIMULATOR

CORS permission lives in response headers. The most famous is Access-Control-Allow-Origin. It can be a single origin, such as https://app.example, or * for public non-credentialed reads. For preflighted requests, the server also answers with allowed methods and allowed request headers.

Common CORS response headers
HeaderWhat it tells the browser
Access-Control-Allow-OriginWhich origin may read this response, or * for public non-credentialed reads.
Access-Control-Allow-MethodsWhich methods a preflighted request may use, such as GET, POST, PUT.
Access-Control-Allow-HeadersWhich non-safelisted request headers are allowed after preflight.
Access-Control-Allow-CredentialsWhether credentialed cross-origin reads are allowed. The value must be true.
Access-Control-Max-AgeHow long the browser may cache the preflight answer.
Access-Control-Expose-HeadersWhich non-safelisted response headers JavaScript may read.
CORS decision simulator
Request and response modelPop out in the code editor (opens in a new tab)JavaScript
const request = {  origin: "https://app.example",  url: "https://api.example/orders/7",  method: "PUT",  headers: { "Content-Type": "application/json", "X-Request-Id": "abc" },  credentials: "include",}; const response = {  "Access-Control-Allow-Origin": "https://app.example",  "Access-Control-Allow-Methods": "GET, POST, PUT",  "Access-Control-Allow-Headers": "Content-Type, X-Request-Id",  "Access-Control-Allow-Credentials": "true",  "Access-Control-Max-Age": "600",  "Vary": "Origin",}; console.log(decideCors(request, response).verdict);
Browser decisionallowed
Shapepreflighted
PreflightOPTIONS /orders/7 | Origin: https://app.example | Access-Control-Request-Method: PUT | Access-Control-Request-Headers: content-type, x-request-id
Exposed?yes
Try it yourself

Preflighted request. Verdict: allowed. The preflight and actual response both grant the browser permission to expose the response.

This is a pure JavaScript model, not a network request. It makes the browser’s decision tree visible.

The simulator is intentionally pure JavaScript. It does not send a request. That makes it testable in Node and lets you change one thing at a time: method, content type, custom header, credentials, and each server response header.

Credentials

Credentials are cookies, HTTP authentication, and client certificates. In fetch terms, they appear when you use options such as credentials: "include" for a cross-origin request. Cookie details come in the Cookies lesson; the CORS rule is the important part here.

With credentials, the server must send Access-Control-Allow-Credentials: true. It must also echo the exact origin in Access-Control-Allow-Origin. * is rejected with credentials because it would be too broad. Servers that echo different origins should usually send Vary: Origin so shared caches do not mix responses between callers.

Credentials checklist
  • Request includes credentials only when you ask for them.
  • Response says Access-Control-Allow-Credentials: true.
  • Response origin is the exact requesting origin, not *.
  • Dynamic origin echoing should include Vary: Origin.

Where you will use this

You will meet CORS when a frontend app talks to an API on another domain or port, when local development runs the frontend and backend on different servers, and when a third-party service must explicitly allow browser clients.

  • In development, http://localhost:3000 asking http://localhost:8080 is cross-origin because the ports differ.
  • A production app at https://app.example asking https://api.example is cross-origin because the hosts differ.
  • A dev proxy can make browser requests look same-origin locally, but production still needs a real server-side CORS policy or same-origin routing.
The frontend cannot grant permission

If the response lacks the right CORS headers, adding request headers from JavaScript will not fix it. It can even trigger a preflight. Configure the API server, a CDN rule, or a proxy you control.

Common misconceptions

  • “CORS stops the request.” Simple requests can still be sent; the browser blocks JavaScript from reading the response.
  • “no-cors fixes it.” It returns an opaque, unreadable response.
  • “The frontend can add Allow-Origin.” That is a response header from the server.
  • “CORS replaces CSRF protection.” It does not; state-changing simple requests can still arrive.
  • “Wildcard works with cookies.” Credentialed requests require an exact origin.
Similar ideas that are easy to mix up
IdeaWhat it means
Same-originNo CORS permission is needed because scheme, host, and port match.
CORS allowedThe request is cross-origin, but response headers grant browser JavaScript access.
CORS blockedThe server may have responded, but the browser does not expose the response to JavaScript.
no-corsFetch resolves with an opaque response that hides status, headers, and body.
Server proxyYour app asks its own origin; your server talks to the other service.

Practice exercises

4 EXERCISES
Exercise 1 · Warm-upWarm-up: compare origins

Predict what the starter code prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
console.log(new URL("https://example.com:443/a").origin === new URL("https://example.com/b").origin);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePractice: simple or preflighted?

    Read the code and decide which word it prints.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const method = "POST";
    const contentType = "application/json";
    const simple = method === "GET" || method === "HEAD" ||
      (method === "POST" && ["text/plain", "multipart/form-data", "application/x-www-form-urlencoded"].includes(contentType));
    console.log(simple ? "simple" : "preflight");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the bug: missing permission

      A cross-origin GET succeeds on the server, but fetch rejects in the browser. The response has Content-Type but no CORS headers. Which response header is missing first?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · ChallengeChallenge: credentials

        A request from https://app.example includes cookies. The server currently sends Access-Control-Allow-Origin: *. What should the allow-origin value be instead?

        Answer, then press Check. Spacing and letter case don’t matter.

          Check your understanding

          7 QUESTIONS
          CORS quiz · 7 questionsScore: first tries count
          1. Question 1 of 7Which parts make up an origin?

            Choose an answer to see the explanation.

          2. Question 2 of 7What does this origin comparison print?

            Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
            console.log(new URL("https://example.com:443/a").origin === new URL("https://example.com/b").origin);

            Choose an answer to see the explanation.

          3. Question 3 of 7Which request normally needs a CORS preflight?

            Choose an answer to see the explanation.

          4. Question 4 of 7A simple cross-origin POST is sent to a server with no CORS headers. What happens?

            Choose an answer to see the explanation.

          5. Question 5 of 7What does fetch(url, { mode: "no-cors" }) give JavaScript for most cross-origin web requests?

            Choose an answer to see the explanation.

          6. Question 6 of 7Which CORS response is valid for a credentialed request from https://app.example?

            Choose an answer to see the explanation.

          7. Question 7 of 7What does this method classifier print?

            Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
            const method = "DELETE";
            const simple = ["GET", "HEAD", "POST"].includes(method);
            console.log(simple ? "simple" : "preflight");

            Choose an answer to see the explanation.

          Key takeaways

          • Origin means scheme, host, and port. Paths are not included.
          • The same-origin policy restricts reading cross-origin responses and other interactions.
          • Simple cross-origin requests can be sent; CORS decides whether JavaScript may read the response.
          • Non-simple requests use a preflight OPTIONS check before the real request.
          • Credentialed CORS requires an exact origin and Access-Control-Allow-Credentials: true.

          CORS is the browser reading a server’s written permission before exposing a cross-origin response to JavaScript.

          Up next: URL & URLSearchParams.

          CompleteFrontend Clear concepts. Working examples.