Representative interview topic

Frontend Interview: How Do You Debug CORS Preflight and Credentialed Requests?

FrontendMedium
Offer.cc Editorial TeamPublished Updated

Question

A page at https://app.example.com uses fetch to send a JSON POST to https://api.example.com/profile/preferences with an X-CSRF-Token header and a login cookie. The call works in curl but fails with a CORS error in the browser. Explain which requests the browser sends, diagnose the configuration, provide a secure fix, and describe how you would verify it.

Question and When It Applies

The page runs at https://app.example.com, while the API is at https://api.example.com. The frontend submits a user preference:

ts
await fetch("https://api.example.com/profile/preferences", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-Token": csrfToken,
  },
  body: JSON.stringify({ compactMode: true }),
})

The server currently returns this response to OPTIONS:

http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST

A direct POST through curl succeeds, but the browser console reports only a cross-origin error. Explain:

  1. why the two subdomains are still cross-origin;
  2. why the browser sends OPTIONS first and when it will proceed with POST;
  3. what is wrong with the current response headers and how to apply a least-privilege fix;
  4. the separate responsibilities of CORS, cookies, authentication, authorization, and CSRF;
  5. how to prove the fix in developer tools and automated tests.

This is useful for frontend and full-stack interviews. The test is not whether someone can recite a header set. It is whether they can work backward from the browser's network phases and separate four questions: was the request sent, were credentials attached, may JavaScript read the response, and did the application authorize the operation?

What the Interviewer Is Evaluating

First, the candidate should identify an origin precisely. An origin consists of scheme, host, and port. The two URLs both use HTTPS and are normally same-site, but their hosts differ, so the request is same-site and cross-origin. CORS is origin-based; the cookie SameSite attribute is site-based. They are not interchangeable.

Second, the candidate should understand preflight triggers. application/json is not a CORS-safelisted request-header value, and X-CSRF-Token is not a safelisted request header. The browser therefore sends OPTIONS to ask whether the actual method and headers are allowed. If preflight fails, the POST is not sent.

Third, the candidate should spot two direct configuration errors. A credentialed response cannot use Access-Control-Allow-Origin: *, and the preflight response does not allow Content-Type or X-CSRF-Token. Listing POST under allowed methods is insufficient.

Finally, the candidate must preserve the security boundary. CORS controls whether the browser shares a cross-origin response with script. It does not replace authentication, authorization, or CSRF protection. The server must not reflect arbitrary origins just to silence the console or allow every method and header.

Questions to Clarify First

  • At which network phase does it fail? No request, OPTIONS only, or a completed POST whose response is

unavailable to JavaScript point to different failures.

  • Must the request carry a cookie? A bearer token or anonymous public resource changes the credential

and allowed-origin policy.

  • Which exact frontend origins are allowed? Production, staging, and local development need explicit

origins, including scheme and port. “The company domain” is not precise enough.

  • Does a gateway, authentication middleware, or redirect intercept OPTIONS? A preflight normally has no

user credentials, so it cannot be required to pass login authentication first.

  • What are the cookie's Domain, Path, Secure, and SameSite attributes? Passing CORS does not guarantee

that the browser sends a cookie.

  • Do actual and error responses all carry CORS headers? If 401, 403, or 500 responses lose the allowed-

origin header, the browser may hide the real status behind a generic CORS error.

  • Is there a CDN or shared cache? If a response varies by Origin, send Vary: Origin so one origin's

response is not reused for another.

30-Second Answer Framework

“I would split this into four gates: origin, preflight permission, credential transport, and response sharing. The hosts differ, so the request is cross-origin. JSON Content-Type and X-CSRF-Token trigger an OPTIONS preflight. The browser sends the POST only if the response permits the exact origin, POST, and both headers. Because fetch uses credentials: include, Allow-Origin cannot be a wildcard. After an exact allowlist match, the server should return https://app.example.com, Allow-Credentials, and Vary: Origin. OPTIONS itself does not depend on the login cookie; the POST still needs authentication, authorization, and CSRF validation. I would verify OPTIONS, POST, cookies, response headers, and application state in the Network panel, then run a positive and hostile-origin test matrix.”

Step-by-Step Deep Dive

Step one: model the state machine the browser actually runs.

The preflight is approximately:

http
OPTIONS /profile/preferences HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-csrf-token

The browser does not ask whether the frontend and backend belong to the same company. It asks whether the response explicitly permits this Origin, method, and header set. The following evidence isolates each phase:

Network evidenceMeaningNext check
No OPTIONS or POSTJavaScript did not run, or an earlier rule such as CSP blocked the URLInspect the call and console first
Only a failed OPTIONSThe POST was not sentInspect status, redirects, and allow headers
OPTIONS succeeds; POST has no cookieCORS permission passed; credential transport did notInspect credentials and cookie attributes
POST returns 401/403/500, but script sees only CORSThe error response lacks valid CORS headersPreserve the status and add the allowed-origin headers
POST succeeds and script can read itThe CORS path passedVerify application state and security negatives

Step two: derive the minimum allow set from the request.

For the single production frontend in the question, return:

http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token
Vary: Origin

The server should compare Origin with an exact allowlist and write it back only after a match. Do not copy the client-supplied Origin unconditionally. If staging and local environments are also allowed, list each complete origin in its corresponding environment. A suffix match can accept a look-alike attacker domain.

The wildcard can be appropriate for a public, anonymous, non-credentialed resource. This user-preference endpoint carries a cookie and performs a sensitive operation, so it needs a specific origin. The preflight OPTIONS carries no cookie. A gateway can handle it before authentication while still strictly validating Origin, method, and request headers.

Step three: inspect the actual request and credentials separately.

The browser sends POST only after a successful preflight. credentials: "include" lets a cross-origin request enter credential processing, but Domain, Path, Secure, SameSite, and browser privacy policies still decide whether a cookie is attached.

These two HTTPS subdomains are normally same-site and cross-origin. SameSite may permit the cookie while CORS still applies. “CORS passed” does not imply “the cookie was sent,” and “the cookie was sent” does not imply “script may read the response.” Inspect Request Cookies, the response status, and CORS response headers as separate facts in the Network panel.

Step four: separate CORS from application security.

CORS is a browser response-sharing protocol, not server-side access control. Curl and server-side HTTP clients do not enforce the browser same-origin policy. A successful curl call proves that the API can handle the request, not that the browser path is correct.

The POST must still:

  • authenticate the user;
  • authorize that user to modify the target resource;
  • validate the CSRF token or use an equally strong defense;
  • validate input and define idempotency or duplicate-submission behavior;
  • return correct CORS headers on allowed-origin 401, 403, and 500 responses so the frontend sees the real error.

“The hostile page cannot read the response” does not mean “the request had no side effect.” Some form-like cross-origin requests do not require preflight and can reach the server. Sensitive writes cannot rely on CORS alone for CSRF protection.

Step five: prove the boundary with positive and negative tests.

After the fix, verify at least:

CaseExpected result
Allowed origin sends the stated requestOPTIONS 204, then POST; script reads the real response
Allowed origin omits the CSRF tokenProtocol configuration determines preflight; application validation rejects the write
Disallowed origin sends the same requestNo CORS header permits that origin; preflight does not pass
Request uses a disallowed method or headerPreflight fails, POST is absent, and state is unchanged
Login cookie is expiredPOST returns a readable 401 and the frontend starts its login flow
Server returns 500Frontend sees 500 and a structured error, not a generic CORS message
Repeated request uses preflight cacheBehavior remains correct; configuration changes are also tested with a cold cache

Correlate this with server logs. A failed preflight must have no matching POST and no preference update. Logs may record Origin, method, header names, and rejection reason, but not cookie or CSRF token values.

Example of a Strong Answer

“I would first identify the network phase. app.example.com and api.example.com have different hosts, so they are cross-origin even though they are same-site. The POST uses JSON Content-Type and X-CSRF-Token, so the browser first sends an OPTIONS request without cookies. It declares the Origin, POST, and the two non-simple headers. The browser sends POST only if the preflight response permits all three.

The current configuration has two direct defects. First, the frontend sets credentials: include, so the credentialed response cannot use a wildcard for Access-Control-Allow-Origin. Second, the response does not include Access-Control-Allow-Headers: Content-Type, X-CSRF-Token. I would validate Origin against an exact server allowlist. For https://app.example.com, I would return that specific value, Allow-Credentials, POST, both allowed headers, and Vary: Origin. OPTIONS can be handled before user-authentication middleware, but a disallowed origin, method, or header is still rejected.

After preflight passes, I would inspect Request Cookies on POST. credentials: include is necessary but not sufficient; Domain, Path, Secure, SameSite, and browser policies still apply. The POST must continue to authenticate, authorize, and validate CSRF because CORS controls response sharing, not access control.

For verification, I would confirm in the Network panel that POST appears only after a successful OPTIONS, then inspect cookies, real status, and response headers. I would test an allowed origin, a disallowed origin, a disallowed method, an expired login, and a 500 response. Server logs must prove that failed preflights have no POST and no state change. Curl is an API baseline, not a substitute for browser CORS evidence.”

Common Mistakes

  • Treating same-site as same-origin → Different subdomains still invoke CORS → **Compare scheme, host,

and port.**

  • Assuming POST in Allow-Methods is enough → The non-simple headers are not allowed → **Check Origin,

Method, and Headers together.**

  • Using Allow-Origin: * with credentials → The browser refuses to expose the response → **Return the

exact allowlisted origin.**

  • Reflecting every requested Origin → Any site may gain read access to credentialed responses → **Match

an exact allowlist first.**

  • Authenticating OPTIONS as a logged-in user → Preflight normally has no credentials → **Perform a

constrained preflight check before user authentication.**

  • Using mode: "no-cors" as a fix → The frontend receives an unreadable opaque response → **Fix the

server's CORS protocol.**

  • Treating CORS as CSRF protection → A request may reach the server and create side effects → **Keep CSRF

and authorization checks on sensitive writes.**

  • Adding CORS headers only to 2xx responses → A 401 or 500 is disguised as a cross-origin error → **Use

consistent CORS headers on errors for allowed origins.**

  • Testing only with curl → Curl does not enforce browser same-origin policy → **Verify with real browser

network evidence.**

Follow-up Questions

Why can a request skip OPTIONS and still fail CORS?

A request using safelisted methods, headers, and Content-Type can be sent directly. The browser still checks the actual response's CORS headers. If response sharing is not allowed, JavaScript cannot read it even though the server may have processed it. Network evidence should distinguish “sent but not shared” from a failed preflight.

Why should Access-Control-Allow-Credentials not appear in the preflight request?

It is a server response header indicating that the browser may expose an actual response made in credentials mode. The client expresses its intent through Fetch's credentials mode; the preflight itself contains no user credentials. The actual response must also satisfy the specific-origin and Allow-Credentials conditions.

What problem does Vary: Origin solve?

When a server chooses Access-Control-Allow-Origin based on the request Origin, a shared cache must include Origin in its cache key. Otherwise, a response for one allowed origin can be reused for another, causing an incorrect denial or disclosure.

Why is the cookie still missing after CORS is fixed?

Check credentials: "include", then inspect the cookie's Domain, Path, Secure, SameSite, expiration, and browser privacy restrictions. CORS permission and cookie transport are adjacent but independent gates.

How should multiple legitimate frontend domains be supported?

Maintain an exact allowlist, validate the complete request Origin, write back the matched value, and send Vary: Origin. Do not return multiple Allow-Origin values or use a permissive regular expression, suffix match, or unconditional reflection.

Can the frontend fix a server CORS failure by itself?

No. The frontend can remove unnecessary non-simple headers or select the intended credentials mode, but the API, reverse proxy, or gateway must return the response headers that permit cross-origin sharing. Production code must not depend on browser extensions or disabling security controls.

How long should preflight be cached?

Choose a duration based on configuration churn, revocation needs, and browser limits. When releasing a CORS change, test both a cold cache and an existing cached path. Caching reduces OPTIONS traffic but can prolong a bad permission, so it must not conceal configuration defects.

Public sources

Related questions