Debug CORS With the Actual Browser Preflight
When a browser blocks your frontend from calling your own API, the visible request is often not
the request that failed. The browser first sent an OPTIONS preflight asking whether this origin,
method, headers, and credential mode were allowed. Your API gave the wrong answer, so the browser
never released the real response to the page.
This pack starts with the network evidence and produces the smallest server-side allowlist change that fixes the intended origin without opening the API to every tab on the internet.
Reproduce the preflight exactly
Copy these values from the browser network panel:
- page origin, including scheme and port;
- API URL;
- request method;
- custom request headers;
- whether cookies or browser credentials are included.
Then replay the preflight:
curl -i -X OPTIONS https://api.example.com/orders \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type,x-request-id'
Check the response, not just the status. It needs the intended
Access-Control-Allow-Origin, method, and headers. When credentials are enabled, the allowed
origin must be explicit. * and credentialed browser requests do not belong together.
The common breakpoints
- Auth middleware rejects
OPTIONSbefore CORS headers are added. - The server allows
http://localhost:3000but the real page uses HTTPS or another port. - A proxy strips the headers emitted by the application.
- The preflight allows
GETbut the page sendsPOSTorPATCH. - The page includes a custom header that is missing from
Access-Control-Allow-Headers. - The server reflects any
Originvalue while also allowing credentials.
CORS is a browser boundary. A successful server-to-server curl request without an Origin
header proves the endpoint works, not that the browser is allowed to expose it to that page.
Use an allowlist, not a mirror
Keep trusted origins in configuration and compare the full origin string. Add the response header
only when the incoming origin is on that list. Send Vary: Origin when a cache may store different
responses per origin.
Do not solve this by turning off browser security, installing a permissive extension, or returning the incoming origin blindly. Those approaches remove the control you were trying to configure.
What the AI does and does not decide
Claude can compare the browser request with server and proxy configuration, point to the first mismatch, and draft a patch. Code must enforce the allowlist. A person should approve any change that expands which websites can make credentialed requests.
The pack keeps both a positive and negative browser test in the plan: the trusted frontend should work, and an unlisted origin should still fail.
Run the starter locally
npm test
npm run validate
npm run sample
Docker Compose runs the Node workflow boundary and n8n. Dry-run validates the evidence and returns a proposed diagnosis without changing a server or proxy.