completefrontend
Open lab My progress
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 originsSay whether two URLs have the same scheme, host, and port.
  • 02
    Predict preflightsSpot which requests make the browser send an OPTIONS check before the real request.
  • 03
    Read CORS headersExplain why the page can or can't read a response, including requests with cookies.

The browser checks for permission

A web page can load many things from other websites: images, fonts, scripts, and data. Loading is one thing. Reading is another. When your JavaScript wants to read an answer from another website, the browser first checks for permission.

That permission system is called CORS. The letters stand for Cross-Origin Resource Sharing. "Cross-origin" means "from a different website". You'll see the exact meaning of "origin" in the next section.

CORS is not new JavaScript syntax. It is also not an option you add to fetch. It works like this:

  1. Your page sends a request to a server. A request is a message that asks for something.
  2. The server sends back a response: the answer.
  3. The response carries headers. A header is a short line of extra facts, written as a name and a value, such as Content-Type: application/json.
  4. The browser looks for one header, Access-Control-Allow-Origin. It says which website may read the response.
  5. If that header is missing, or names a different website, the browser does not give the response to your code.
Real-life analogyThe apartment mailroom

In an apartment building, the mailroom clerk hands you mail for your own apartment, 4A. You can't open mail for your neighbour in 4B. But your neighbour can leave a note at the desk: "4A may open my mail." With that note, the clerk hands it over. Without the note, the clerk keeps the envelope closed.

In real life: Mail for your own apartment, 4A
In JavaScript: A response from your own origin
In real life: Mail for your neighbour in 4B
In JavaScript: A response from another origin
In real life: A note from 4B: "4A may open my mail"
In JavaScript: The Access-Control-Allow-Origin header
In real life: The mailroom clerk
In JavaScript: The browser, which checks the rule
Four questions to ask when a request fails
  1. Is the page asking a different origin?
  2. Is the request simple, or does it need a preflight? (Both words are explained below.)
  3. Do the response headers give permission?
  4. Does the request send cookies or other credentials?

The same-origin policy

INTERACTIVE

An origin is three parts of a URL:

  • The scheme: http or https.
  • The host: the site's name, like example.com.
  • The port: a number that picks a door on the server, like 3000. If the URL doesn't show one, https uses 443 and http uses 80.

The origin of https://example.com:443/products?q=shoes is https://example.com. The port 443 is the default for https, so it is left out. The path /products and the query ?q=shoes are not part of the origin.

The URL https://example.com:443/products?q=shoes split into cards. The cards https, example.com, and 443 are the scheme, host, and port, and together they are the origin. The card /products?q=shoes is the path and query, and it is not part of the origin.https://example.com:443/products?q=shoeshttpsexample.com443schemehostportthe origin/products?q=shoespath and querynot part
The origin is the scheme, the host, and the port. The path and the query after them are not part of it.

The same-origin policy is a safety rule in every browser. JavaScript may read answers from its own origin. To read an answer from a different origin, it needs CORS permission.

Why? Any site you visit can run JavaScript in your browser. Without this rule, a random site could quietly read your bank page or your company's private pages, because your browser is signed in to them.

Change the scheme, the host, or the portPop out in the code editor (opens in a new tab)JavaScript
const app = new URL("http://shop.example:3000/cart");
const api = new URL("https://shop.example:3000/cart");
const store = new URL("http://www.shop.example:3000/cart");

console.log(app.origin === api.origin);
console.log(app.origin === store.origin);
console.log(new URL("http://shop.example:3000").origin === new URL("http://shop.example:5173").origin);

new URL(...) reads a URL and splits it into parts. Its .origin property is the origin as text.

  • Line 5 compares http with https. The schemes differ, so it prints false.
  • Line 6 compares shop.example with www.shop.example. The hosts differ, so it prints false.
  • Line 7 compares port 3000 with port 5173. The ports differ, so it prints false.

One different part is enough. If only the path were different, the comparison would print true.

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;}
Resultdifferent
A originhttps://shop.example
B originhttp://shop.example
DecisionDifferent origins
Try it yourself

Different origins: the scheme, the host, or the port is different.

Type two URLs, or pick a pair. The code uses new URL(), the same URL reader the browser uses.

Watch for these cases. Each pair is a different origin:

  • http and https: different schemes.
  • www.example.com and example.com: different hosts.
  • localhost:3000 and localhost:5173: different ports.
  • localhost and 127.0.0.1: they reach the same computer, but the host text is different.

A real CORS failure, without leaving this site

REAL BROWSER

This lesson doesn't call any outside website. It can still make a real cross-origin request, safely.

The trick is an iframe: a small page inside this page. The iframe below is sandboxed, which means the browser limits what it may do. It has sandbox="allow-scripts" but not allow-same-origin. So the browser gives it a special origin that matches nothing. This is called an opaque origin, and the browser shows it as null. From inside the iframe, this site is a different origin.

A real cross-origin fetch from a sandboxed iframe
The fetch code inside the iframePop 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 iframeloading

Click a button to run a real fetch inside the iframe.

Try it yourself

The iframe is sandboxed without allow-same-origin, so its origin is null. Fetching this site from inside it is a cross-origin request, even though the iframe and this page come from the same site.

Real browser code inside the iframe reports each result. The error text differs between browsers, so you see exactly what yours says.

Each button runs a different fetch inside the iframe:

  • Normal fetch: this site's response has no Access-Control-Allow-Origin header for null. So the promise from fetch fails (it rejects) with a TypeError. The message text is different in each browser.
  • Fetch with no-cors: the option mode: "no-cors" does not fix CORS. It gives JavaScript an opaque response: a response whose status, headers, and body are all hidden.
  • Fetch a data: URL: a data: URL holds its text inside the URL itself. It is not a cross-origin request, so you can read it. Compare it with the other two.
What a no-cors response looks likePop out in the code editor (opens in a new tab)JavaScript
function noCorsResult() {
  return { type: "opaque", status: 0, bodyReadable: false };
}

const response = noCorsResult();
console.log(response.type);
console.log(response.status);
console.log(response.bodyReadable);

This small model prints opaque, then 0, then false. The status is 0 and the body can't be read. So no-cors is not a way to read data from an API.

The server gets the request, but the page can't read the answerPop out in the code editor (opens in a new tab)JavaScript
const serverReceivedRequest = true;
const browserCanReadResponse = false;

console.log(serverReceivedRequest);
console.log(browserCanReadResponse);

Line 4 prints true: the server got the request. Line 5 prints false: the browser did not let the page read the response. Both can be true at the same time.

Left: a window called app page. Right: a window called server. A request arrow goes from the page to the server, and the server shows received: true. A response arrow comes back with no Allow-Origin header, and the page shows can read: false.app pagecan read: falseserverreceived: truerequestresponseno Allow-Origin
The request reaches the server, and the server answers. The answer has no permission header, so the browser keeps it away from the page.

CORS is a browser rule. A tool like curl, which sends requests from a terminal, doesn't check it. So when you debug, look in two places:

  • The Network panel in the browser's DevTools, to see the request and the response headers.
  • The server's logs, to see whether the request arrived.
How the browser decides: does the header name this page?Pop out in the code editor (opens in a new tab)JavaScript
function canBrowserRead(requestOrigin, responseHeaders) {
  return responseHeaders["Access-Control-Allow-Origin"] === requestOrigin;
}

console.log(canBrowserRead("https://app.example", { "Access-Control-Allow-Origin": "https://app.example" }));
console.log(canBrowserRead("https://app.example", { "Access-Control-Allow-Origin": "https://other.example" }));

canBrowserRead compares the page's origin with the response's Access-Control-Allow-Origin header.

  • Line 5: the header names https://app.example, the same as the page. It prints true.
  • Line 6: the header names https://other.example. It prints false.

This only models whether the page may read the response. The server got the request in both cases.

Simple requests and preflight requests

STEP THROUGH

Some cross-origin requests are called simple requests. The browser sends them straight away. "Simple" is just an old name. It does not mean "safe". A request is simple when all of these are true:

  • The method is GET, HEAD, or POST.
  • Every request header is on a short allowed list. The spec calls these headers safelisted. Accept and Content-Type are on the list; Authorization and your own headers like X-Request-Id are not.
  • If it sets Content-Type (the kind of data in the body), the value is one of these three: application/x-www-form-urlencoded, multipart/form-data, or text/plain.

Any other request needs a preflight: a check before the real request. The browser does it for you:

  1. The browser sends an OPTIONS request. It asks: "May I send this method with these headers?" It names them in Access-Control-Request-Method and Access-Control-Request-Headers.
  2. The server answers with what it allows.
  3. If the answer is yes, the browser sends the real request. If not, it stops there.
Left: a window called app page that wants to send a PUT with JSON. Right: a window called server that checks Allow-Methods. Arrow 1, OPTIONS, goes to the server. Arrow 2, PUT allowed, comes back. Arrow 3, the real PUT, goes to the server.app pagewants to sendPUT + JSONserverchecksAllow-Methods1. OPTIONS2. PUT allowed3. real PUT
For a PUT with JSON, the browser first sends an OPTIONS request to ask. Only after the server says yes does it send the real PUT.
A POST with JSON needs a preflightPop out in the code editor (opens in a new tab)JavaScript
const request = {
  method: "POST",
  headers: { "Content-Type": "application/json" },
};

const simpleMethods = ["GET", "HEAD", "POST"];
const simpleTypes = ["text/plain", "multipart/form-data", "application/x-www-form-urlencoded"];
const simple = simpleMethods.includes(request.method) && simpleTypes.includes(request.headers["Content-Type"]);

console.log(simple ? "send directly" : "preflight first");

The method is POST, which is on the simple list. But the Content-Type is application/json, which is not one of the three allowed values. So the code prints preflight first.

An Authorization header, or any header you make up, also needs a preflight. That is true even with GET.

Sort four requests: simple or preflight?Pop out in the code editor (opens in a new tab)JavaScript
function requestKind(method, headers = {}) {
  const simpleMethods = ["GET", "HEAD", "POST"];
  const contentType = headers["Content-Type"];
  const simpleTypes = ["text/plain", "multipart/form-data", "application/x-www-form-urlencoded"];
  const headersAreSafe = Object.keys(headers).every((name) => name === "Content-Type" && simpleTypes.includes(contentType));
  return simpleMethods.includes(method) && headersAreSafe ? "simple" : "preflight";
}

console.log(requestKind("GET"));
console.log(requestKind("PUT"));
console.log(requestKind("POST", { "Content-Type": "application/json" }));
console.log(requestKind("GET", { Authorization: "Bearer token" }));
  • Line 9, a plain GET, prints simple.
  • Line 10, a PUT, prints preflight: PUT is not a simple method.
  • Line 11, a POST with JSON, prints preflight.
  • Line 12, a GET with an Authorization header, prints preflight.

A common mistake is to look only at the method. The headers count too.

Real-life analogyCalling ahead before a big delivery

You can drop a postcard at the front desk without asking. But before you bring a big crate with special paperwork, you phone first: "I'm bringing a crate like this. Is that OK?" The desk can write down the answer, so you don't have to phone again for a while.

In real life: A normal postcard
In JavaScript: A simple GET, or a POST like a form sends
In real life: A big crate with special paperwork
In JavaScript: A PUT, or a request with JSON or your own header
In real life: Phoning the front desk first
In JavaScript: The preflight OPTIONS request
In real life: The desk writes down "crates are fine" for a week
In JavaScript: Access-Control-Max-Age
Preflight or not?
  • GET with no extra headers
  • POST with text/plain
  • POST with application/json
  • PUT
  • GET with an Authorization header
  • POST with FormData
Try it yourself
0 of 6 correct

Sort each cross-origin request: does the browser send it straight away, or check first with a preflight?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Now step through a full preflight. Pick an answer from the server, then step through the browser's checks.

Step through a PUT that needs a preflight
Step 0 of 5Ready
Your turn: follow the blue line

Guess first: will the browser let the page read the response? Then step through the browser's checks.

Running in
  1. script
Next: line 1
  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);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick the server's answer
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.

The CORS headers

SIMULATOR

The permission is always in the response headers, which the server sends.

The main one is Access-Control-Allow-Origin. Its value can be one origin, such as https://app.example. Or it can be *, which means "any origin". * only works for requests without cookies; the Credentials section explains why.

For a preflight, the server also says which methods and which request headers it allows.

Each row below is one response header. The right column says what it tells the browser.

The CORS response headers and what each one means
Response headerWhat it tells the browser
Access-Control-Allow-OriginWhich origin may read this response. * means any origin, but only for requests without cookies.
Access-Control-Allow-MethodsWhich methods are allowed after a preflight, such as GET, POST, PUT.
Access-Control-Allow-HeadersWhich request headers outside the allowed list may be sent after a preflight.
Access-Control-Allow-CredentialsWhether a request with cookies may be read. The value must be true.
Access-Control-Max-AgeHow many seconds the browser may remember the preflight answer.
Access-Control-Expose-HeadersWhich extra response headers JavaScript may read.
A custom response header needs its own permissionPop out in the code editor (opens in a new tab)JavaScript
const responseHeaders = {
  "Content-Type": "application/json",
  "X-Request-Id": "abc123",
};

function canReadResponseHeader(name, exposedHeaders) {
  const safe = ["content-type", "cache-control"].includes(name.toLowerCase());
  const exposed = exposedHeaders.toLowerCase().split(",").map((item) => item.trim());
  return safe || exposed.includes(name.toLowerCase());
}

console.log(canReadResponseHeader("Content-Type", ""));
console.log(canReadResponseHeader("X-Request-Id", "X-Request-Id"));

Even when the page may read a response, it can't read every header on it.

  • Line 12: Content-Type is on the short list of response headers JavaScript can always read. It prints true.
  • Line 13: X-Request-Id is a custom header. It can be read only because the server named it in Access-Control-Expose-Headers: X-Request-Id. It prints true.

This permission is separate from the permission to read the response itself.

CORS decision simulator
The request and the server's answerPop 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
Kindneeds a preflight
Preflight sentOPTIONS /orders/7 | Origin: https://app.example | Access-Control-Request-Method: PUT | Access-Control-Request-Headers: content-type, x-request-id
Page can read?yes
Try it yourself

Needs a preflight. Result: allowed. The preflight answer and the real answer both give permission, so the page can read the response.

This is plain JavaScript. It sends no request; it shows each check the browser makes.

The simulator is plain JavaScript. It doesn't send any request. Change one setting at a time: the method, the content type, the custom header, cookies, or one of the server's headers. Then read the result.

Cookies and other credentials

Credentials are things that prove who you are to a server. There are three kinds:

  • Cookies: small pieces of text a site saves in your browser, often to keep you signed in.
  • HTTP authentication: a username and password the browser sends for you.
  • Client certificates: digital ID files installed in the browser.

For a cross-origin request, fetch sends credentials only when you ask for them with credentials: "include". The Cookies lesson covers cookies in detail. Here, the CORS rule is what matters.

When a request includes credentials, the server must send two things:

  • Access-Control-Allow-Credentials: true.
  • The exact origin of the page in Access-Control-Allow-Origin. * is refused.

The browser refuses * here because it would let every site read your private, signed-in data.

A server often copies the page's origin into its answer, so the answer is different for each origin. Then it should also send Vary: Origin. A cache is a store of saved responses, kept to save time. Vary: Origin tells caches: "this answer depends on the origin, so don't give one origin's answer to another."

Checklist for requests with credentials
  • The request sends credentials only when you ask for them.
  • The response says Access-Control-Allow-Credentials: true.
  • The response names the exact origin of the page, not *.
  • If the server copies the origin into its answer, it also sends Vary: Origin.
With credentials, * is refusedPop out in the code editor (opens in a new tab)JavaScript
function canReadWithCredentials(origin, credentials, allowOrigin, allowCredentials) {
  if (credentials !== "include") return allowOrigin === "*" || allowOrigin === origin;
  return allowOrigin === origin && allowCredentials === "true";
}

console.log(canReadWithCredentials("https://app.example", "include", "*", "true"));
console.log(canReadWithCredentials("https://app.example", "include", "https://app.example", "true"));

Both calls send credentials ("include"). Both answers say credentials are allowed ("true").

  • Line 6: the answer uses *. It prints false.
  • Line 7: the answer names https://app.example. It prints true.
Two requests from https://app.example that send cookies. Left: the answer says Allow-Origin star, and the result is false, blocked. Right: the answer says Allow-Origin https://app.example, and the result is true, the page can read it.Allow-Origin*false: blockedAllow-Originhttps://app.exampletrue: can read
Both requests send cookies, and both answers say credentials are allowed. Only the exact origin passes; * is refused.

So an API that keeps users signed in with cookies, such as a profile API, must name the page's exact origin.

Where you will use this

You'll meet CORS in these places:

  • On your own computer, while you build an app. http://localhost:3000 asking http://localhost:8080 is cross-origin, because the ports differ.
  • On a live site. https://app.example asking https://api.example is cross-origin, because the hosts differ.
  • When you use another company's service from the browser. That service must give your origin permission.
Page code can't give itself permission

If the response doesn't have the right CORS headers, adding request headers in JavaScript won't help. It can even cause a preflight. Fix it where the response is made: on the API server, in a CDN rule (a CDN is a network of servers that delivers your files), or in a proxy you control.

A proxy is a server that passes requests on to another server. During development, a dev proxy runs on the same origin as your page. Your page asks the proxy, and the proxy asks the API.

A dev proxy keeps the page on its own originPop out in the code editor (opens in a new tab)JavaScript
const frontend = "http://localhost:3000";
const api = "http://localhost:8080/orders";
const browserUrl = new URL("/api/orders", frontend);

console.log(browserUrl.origin === frontend);
console.log(new URL(api).origin === frontend);
  • Line 5 prints true. The page calls /api/orders on its own origin, localhost:3000.
  • Line 6 prints false. Calling the API on port 8080 directly would be cross-origin.

The proxy then sends the request on to the API. Server-to-server requests don't go through the browser's CORS check. A dev proxy only helps on your own computer. A live site still needs real CORS headers on the server, or the page and API on the same origin.

Common mistakes

  • "CORS stops the request." A simple request is still sent, and the server can act on it. CORS stops the page from reading the response.
  • "no-cors fixes it." It gives an opaque response. The status, headers, and body are all hidden.
  • "The page can add Allow-Origin." Access-Control-Allow-Origin is a response header. Only the server can send it.
  • "CORS protects against CSRF." CSRF is an attack where another site makes your browser send a request that changes something, like a payment. CORS doesn't stop that, because simple requests are still sent.
  • "* works with cookies." A request with credentials needs the exact origin.

These five situations are easy to mix up. Each row says what happens and whether the page can read the response.

Five situations, and what the page can read in each
SituationWhat happens
Same originScheme, host, and port all match. No CORS permission is needed, and the page can read the response.
Cross-origin, allowedThe origins differ, but the response headers give permission. The page can read the response.
Cross-origin, blockedThe server may have answered, but the browser keeps the response from the page. fetch rejects with a TypeError.
no-corsfetch gives an opaque response. The status, headers, and body are all hidden.
Server proxyThe page asks its own origin. Your server then asks the other service, with no browser CORS check.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upWarm-up: compare two 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 preflight?

    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: no permission

      A page sends a cross-origin GET. The server handles it fine, but fetch fails in the browser. The response has a Content-Type header and no CORS headers. Which response header is missing?

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

        Exercise 4 · ChallengeChallenge: credentials

        A page on https://app.example sends a request with cookies. The server answers with Access-Control-Allow-Origin: *. What should the Access-Control-Allow-Origin value be instead?

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

          Exercise 5 · ChallengeApply it: read an ID on a profile page

          A profile API sends an X-Request-Id header, which the support team uses to find a request. The page may read the response, but response.headers.get("X-Request-Id") returns null. Which response header makes the ID readable?

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

            Check your understanding

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

              Choose an answer to see the explanation.

            2. Question 2 of 8What 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 8Which cross-origin request needs a preflight?

              Choose an answer to see the explanation.

            4. Question 4 of 8A page sends a simple cross-origin POST. The server's answer has no CORS headers. What happens?

              Choose an answer to see the explanation.

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

              Choose an answer to see the explanation.

            6. Question 6 of 8A request from https://app.example sends cookies. Which answer from the server lets the page read the response?

              Choose an answer to see the explanation.

            7. Question 7 of 8The page may read a response, but JavaScript can't read its X-Request-Id header. What is missing?

              Choose an answer to see the explanation.

            8. Question 8 of 8What does this method check 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

            • An origin is the scheme, host, and port. The path is not part of it.
            • The same-origin policy stops a page from reading answers from another origin without permission.
            • A simple cross-origin request is still sent. CORS decides whether the page may read the response.
            • Other requests get a preflight: an OPTIONS check before the real request.
            • Access-Control-Expose-Headers lets the page read extra response headers.
            • With credentials, the server must name the exact origin and send Access-Control-Allow-Credentials: true.
            • Fix CORS on the API server, a CDN, or a proxy. Page JavaScript can't give itself permission.

            CORS is the browser checking a server's permission headers before it lets your JavaScript read an answer from another origin.

            Up next: URL & URLSearchParams.

            CompleteFrontend Clear concepts. Working examples.