'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: 600to 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 PUTRemember
- 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.