Same-origin policy and CORS

HTTP & the web · Practical · 14 min

'Blocked by CORS policy' is the most common error front-end developers hit when calling an API.

The same-origin policy stops a page's scripts from reading responses from other origins (scheme + host + port). Otherwise any website you visit could use your logged-in cookies to read your email from another tab.

CORS (Cross-Origin Resource Sharing) lets a server relax this for chosen origins. The browser sends an Origin header with cross-origin requests; the server answers with Access-Control-Allow-Origin: https://app.example.com (or *). If the header doesn't match, the browser hides the response from JavaScript. The request may still have reached the server; CORS protects reading, not sending.

Preflight. "Simple" requests (GET, HEAD, POST with form-like content types and no custom headers) are sent directly. Anything else, such as PUT, DELETE, a JSON Content-Type or an Authorization header, triggers an automatic OPTIONS preflight first. The server must answer it with:

  • Access-Control-Allow-Methods: GET, POST, PUT, DELETE,
  • Access-Control-Allow-Headers: Content-Type, Authorization,
  • optionally Access-Control-Max-Age: 600 to cache the preflight.

Credentials. To send cookies cross-origin, the client sets credentials: "include" and the server must return Access-Control-Allow-Credentials: true with an explicit origin; * is not allowed with credentials. If the allowed origin varies per request, add Vary: Origin so caches don't mix them up.

CORS is enforced only by browsers. curl, servers and mobile apps ignore it, so it's not an authorization mechanism. The fix for a CORS error is always on the server (or a same-origin proxy), never in the front-end code.

Example

import http from "node:http";

const ALLOWED = new Set(["https://app.example.com"]);

const server = http.createServer((req, res) => {
  const origin = req.headers.origin;
  if (ALLOWED.has(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.setHeader("Vary", "Origin");
  }
  if (req.method === "OPTIONS") {                       // preflight
    res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
    res.setHeader("Access-Control-Max-Age", "600");
    res.statusCode = 204; return res.end();
  }
  res.setHeader("Content-Type", "application/json");
  res.end(JSON.stringify({ ok: true }));
});
server.listen(8080);

// In the browser at https://app.example.com:
// fetch("https://api.example.com/items", { method: "PUT", credentials: "include",
//   headers: { "Content-Type": "application/json" }, body: "{}" })  -> OPTIONS, then PUT

Remember

  • Same-origin policy blocks reading cross-origin responses in browsers
  • Servers opt in with Access-Control-Allow-Origin
  • Non-simple requests trigger an OPTIONS preflight
  • Credentials need an explicit origin, not *, plus Allow-Credentials

Watch out for

  • Trying to 'fix' CORS in front-end code; the server must send the headers.
  • Treating CORS as API security; non-browser clients ignore it.

Check yourself

  1. A fetch with Content-Type: application/json to another origin fails before the POST is sent. What did the browser send first?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS

Builds on

Unlocks

Nothing builds on this yet.