What a CORS Bug Taught Me About Debugging
A production CORS issue forced me to slow down, isolate the layers, and debug the system instead of guessing at headers.
Maruf Hossain4 min read
It was late, the project was almost ready, and the local version worked.
The frontend talked to Payload CMS. Clerk authentication worked. The demo path looked fine. Then I deployed the app, refreshed the page, and got the error every web developer eventually meets:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource...My first reaction was to search for the fastest fix. Add a header. Try Access-Control-Allow-Origin: *. Change one config, refresh, repeat.
That did not work.
The useful lesson was not "CORS is annoying." The useful lesson was that I was debugging by guessing.
Stop Changing Everything
My first mistake was treating CORS like a single setting. It is not. A request passes through the browser, the frontend, the backend, auth middleware, hosting, and sometimes a proxy or CDN before the browser decides whether the response is allowed.
Changing random headers made the problem harder to understand because I could no longer tell which change mattered.
Once I stopped editing and started observing, the issue became easier to reason about.
Find the Request Before the Fix
A CORS message is a browser report, not a diagnosis. The request might have reached the server and returned an error without the right CORS headers. A preflight OPTIONS request might have failed before the real request was sent. A redirect might have moved the request to an origin I never configured.
The Network tab separates those cases. I started checking:
- the request URL and
Originheader - whether an
OPTIONSrequest ran first - the response status before the browser hid the body
Access-Control-Allow-OriginandAccess-Control-Allow-Credentials- whether the response came from the application or an intermediate proxy
That checklist was more useful than copying another header into the config.
Reproduce the Failure
The app worked locally, but failed in production. That narrowed the problem immediately.
Then I tested the same flow across browsers and watched the Network tab instead of only reading the console message. The important question became:
Which request is failing, and what headers does the browser actually receive?
That shifted the debugging from "CORS is broken" to a more useful question: "Where are the expected headers disappearing?"
Check the Layers
I broke the system down into layers:
- browser enforcement
- frontend request configuration
- Payload allowed origins
- Clerk authentication settings
- deployment/proxy behavior
The breakthrough came when I stopped blaming the frontend and checked the response path. The production setup was not returning the headers the browser needed, even though the application config looked correct.
That is the part I almost missed. The code can be right and the deployed response can still be wrong.
The Fix
The final fix was to use specific production origins and make the browser and server agree about credentials. A simplified Payload configuration looked like this:
// payload.config.ts
import { buildConfig } from "payload";
const allowedOrigins = [
process.env.CLERK_FRONTEND_URL,
process.env.NEXT_PUBLIC_APP_URL,
].filter((origin): origin is string => Boolean(origin));
export default buildConfig({
cors: allowedOrigins,
csrf: allowedOrigins,
// ...the rest of the Payload config
});The client also had to opt into sending credentials when the request needed the authenticated session:
await fetch(`${apiUrl}/api/example`, {
credentials: "include",
});When a request includes credentials, Access-Control-Allow-Origin: * is not a valid shortcut. The server must return a specific allowed origin, and the browser must receive the credential headers it expects. Payload also treats CORS and CSRF as separate configuration concerns: one controls which origins can read API responses, while the other controls which origins can make cookie-authenticated requests.
That detail matters because auth flows often depend on cookies or credentials. A loose CORS config might look convenient, but it will either fail or weaken the security model.
What I Would Do First Next Time
- Open the Network tab before changing config.
- Inspect the preflight and actual request separately.
- Compare local and production response headers.
- Confirm the exact origin used by the deployed frontend.
- Check auth and proxy settings before assuming the frontend is wrong.
- Write down the final working config.
The bug was frustrating, but it gave me a better debugging habit: slow down, identify the layer, and change one thing at a time.
That sounds obvious after the fact. It was not obvious at 2 AM.
— Maruf