Fix the specific, maddening case where a request works from curl or Postman but the browser blocks it with a CORS error. CORS is enforced by the browser, not the server — so the failure is always that the server isn't returning the response headers the browser requires for a cross-origin request. This skill reads the exact error, determines what the browser is asking for, and sets the minimal correct headers on the server, without disabling the protection or opening the API to every origin.
[!NOTE]
CORS is a browser policy, not a server-side security control. Loosening it doesn't make your API safer or more dangerous to non-browser clients — but a wildcard-with-credentials or reflect-any-origin "fix" does expose authenticated users to malicious sites. Scope it to the origins you actually serve.
When to use this skill
- A browser
fetch/XHR fails with "No 'Access-Control-Allow-Origin' header" or "…does not pass access control check", but the same endpoint works from curl.
- A request started failing after you added
Authorization headers, custom headers, cookies, or switched to PUT/DELETE (it became preflighted).
- The preflight
OPTIONS request returns 404/405 or the wrong headers.
- You set
Access-Control-Allow-Origin: * and it broke once credentials were involved.
Instructions
- Read the exact error and the request. Get the precise browser console message and the request's origin, method, and headers. The error text names what's missing (origin not allowed, method not allowed, header not allowed, credentials mismatch).
- Classify simple vs. preflighted. A simple request (GET/HEAD/POST with only safelisted headers and content types) needs only the right headers on the actual response. A preflighted request (custom headers,
Authorization, JSON body via non-safelisted content type, or PUT/PATCH/DELETE) first sends an OPTIONS preflight — the server must answer that too.
- Find where responses are produced. Locate the CORS middleware or the place response headers are set (framework CORS package, a reverse proxy, or manual header code). Confirm whether a proxy in front is adding or stripping CORS headers — duplicated or conflicting headers are a common cause.
- Set the origin correctly. For requests without credentials you may use
Access-Control-Allow-Origin: *. For requests with credentials, you must echo a specific allowlisted origin (never *), add Access-Control-Allow-Credentials: true, and add Vary: Origin so caching stays correct. Check the incoming Origin against an explicit allowlist; don't reflect arbitrary origins.
- Answer the preflight. Ensure
OPTIONS on the route returns 204/200 with Access-Control-Allow-Methods (the methods you allow), Access-Control-Allow-Headers (echo or list the requested headers, e.g. Authorization, Content-Type), and Access-Control-Max-Age to cache the preflight and cut round-trips. Make sure the OPTIONS route isn't swallowed by auth middleware or a 404.
- Prefer the framework's CORS support. Use the maintained CORS middleware (e.g.
cors for Express, CORSMiddleware for FastAPI/Starlette, the framework's config) with an explicit origin allowlist rather than hand-writing headers on every route.
- Verify with a real preflight. Reproduce the preflight from the shell and confirm the headers, then re-test in the browser:
curl -i -X OPTIONS https://api.example.com/things \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: authorization,content-type"
The response must include Access-Control-Allow-Origin: https://app.example.com (or * when credential-free), the allowed method, and the allowed headers.
Examples
An Express API called from https://app.example.com with a cookie session, currently sending Access-Control-Allow-Origin: * and failing once credentials were added:
import cors from "cors";
const allowedOrigins = ["https://app.example.com", "http://localhost:3000"];
app.use(
cors({
origin(origin, cb) {
// allow same-origin/non-browser (no Origin) and allowlisted origins
if (!origin || allowedOrigins.includes(origin)) return cb(null, true);
return cb(new Error("Not allowed by CORS"));
},
credentials: true, // echoes a specific origin, not *
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Authorization", "Content-Type"],
maxAge: 86400,
})
);
Report the outcome: what the browser was actually rejecting (origin / method / header / credentials), the exact headers now returned, that the OPTIONS preflight succeeds, and the allowlist you scoped it to — plus a note that origins are an explicit list, not a reflect-all.
1---2name: cors-configurator3description: Diagnose and fix a failing cross-origin browser request — read the exact console error, work out whether it's a simple or preflighted request, and set the minimal correct Access-Control-* response headers (including the wildcard-with-credentials trap and the OPTIONS preflight) on the server. Use when the browser blocks a fetch/XHR with a CORS error but the API itself works from curl or Postman.4---56Fix the specific, maddening case where a request works from curl or Postman but the browser blocks it with a CORS error. CORS is enforced by the browser, not the server — so the failure is always that the server isn't returning the response headers the browser requires for a cross-origin request. This skill reads the exact error, determines what the browser is asking for, and sets the minimal correct headers on the server, without disabling the protection or opening the API to every origin.78> [!NOTE]9> CORS is a browser policy, not a server-side security control. Loosening it doesn't make your API safer or more dangerous to non-browser clients — but a wildcard-with-credentials or reflect-any-origin "fix" does expose authenticated users to malicious sites. Scope it to the origins you actually serve.1011## When to use this skill1213- A browser `fetch`/XHR fails with "No 'Access-Control-Allow-Origin' header" or "…does not pass access control check", but the same endpoint works from curl.14- A request started failing after you added `Authorization` headers, custom headers, cookies, or switched to `PUT`/`DELETE` (it became preflighted).15- The preflight `OPTIONS` request returns 404/405 or the wrong headers.16- You set `Access-Control-Allow-Origin: *` and it broke once credentials were involved.1718## Instructions19201. **Read the exact error and the request.** Get the precise browser console message and the request's origin, method, and headers. The error text names what's missing (origin not allowed, method not allowed, header not allowed, credentials mismatch).212. **Classify simple vs. preflighted.** A *simple* request (GET/HEAD/POST with only safelisted headers and content types) needs only the right headers on the actual response. A *preflighted* request (custom headers, `Authorization`, JSON body via non-safelisted content type, or `PUT`/`PATCH`/`DELETE`) first sends an `OPTIONS` preflight — the server must answer that too.223. **Find where responses are produced.** Locate the CORS middleware or the place response headers are set (framework CORS package, a reverse proxy, or manual header code). Confirm whether a proxy in front is adding or stripping CORS headers — duplicated or conflicting headers are a common cause.234. **Set the origin correctly.** For requests without credentials you may use `Access-Control-Allow-Origin: *`. For requests *with* credentials, you must echo a specific allowlisted origin (never `*`), add `Access-Control-Allow-Credentials: true`, and add `Vary: Origin` so caching stays correct. Check the incoming `Origin` against an explicit allowlist; don't reflect arbitrary origins.245. **Answer the preflight.** Ensure `OPTIONS` on the route returns `204`/`200` with `Access-Control-Allow-Methods` (the methods you allow), `Access-Control-Allow-Headers` (echo or list the requested headers, e.g. `Authorization, Content-Type`), and `Access-Control-Max-Age` to cache the preflight and cut round-trips. Make sure the OPTIONS route isn't swallowed by auth middleware or a 404.256. **Prefer the framework's CORS support.** Use the maintained CORS middleware (e.g. `cors` for Express, `CORSMiddleware` for FastAPI/Starlette, the framework's config) with an explicit origin allowlist rather than hand-writing headers on every route.267. **Verify with a real preflight.** Reproduce the preflight from the shell and confirm the headers, then re-test in the browser:27 ```bash28 curl -i -X OPTIONS https://api.example.com/things \29 -H "Origin: https://app.example.com" \30 -H "Access-Control-Request-Method: PUT" \31 -H "Access-Control-Request-Headers: authorization,content-type"32 ```33 The response must include `Access-Control-Allow-Origin: https://app.example.com` (or `*` when credential-free), the allowed method, and the allowed headers.3435## Examples3637An Express API called from `https://app.example.com` with a cookie session, currently sending `Access-Control-Allow-Origin: *` and failing once credentials were added:3839```js40import cors from "cors";4142const allowedOrigins = ["https://app.example.com", "http://localhost:3000"];4344app.use(45 cors({46 origin(origin, cb) {47 // allow same-origin/non-browser (no Origin) and allowlisted origins48 if (!origin || allowedOrigins.includes(origin)) return cb(null, true);49 return cb(new Error("Not allowed by CORS"));50 },51 credentials: true, // echoes a specific origin, not *52 methods: ["GET", "POST", "PUT", "DELETE"],53 allowedHeaders: ["Authorization", "Content-Type"],54 maxAge: 86400,55 })56);57```5859Report the outcome: what the browser was actually rejecting (origin / method / header / credentials), the exact headers now returned, that the `OPTIONS` preflight succeeds, and the allowlist you scoped it to — plus a note that origins are an explicit list, not a reflect-all.