Why Does a POST Sometimes Send Two Requests? A Deep Dive into CORS Preflight

This article explains why browsers may issue a duplicate POST request by exploring the same‑origin policy, the mechanics of CORS, the criteria for simple requests, the structure of preflight OPTIONS requests, credential handling, and how development tools like Webpack Dev Server bypass these restrictions.

AI Illustrated Series
AI Illustrated Series
AI Illustrated Series
Why Does a POST Sometimes Send Two Requests? A Deep Dive into CORS Preflight

Interview Context

During technical interviews, candidates often encounter quirky questions; one such question asks why a POST request can be sent twice.

1. Same‑Origin Policy

The browser treats any loaded resource—JavaScript, images, audio, video, or even executable files—as potentially unsafe if it originates from a different scheme, host, or port. To protect users from attacks such as XSS, SQL injection, CSRF, and others, browsers enforce the same‑origin policy.

Two URLs are considered same‑origin only when their protocol, host, and port match. For example, the URL http://store.company.com:80/dir/page.html is compared against other URLs in the following table (illustrated in the image below).

The same‑origin policy restricts three aspects:

DOM access: scripts cannot read or manipulate the DOM of a page from a different origin.

Web data access: XMLHttpRequest and Fetch can only target resources sharing the same origin.

Network communication: browsers block cross‑origin network responses unless explicitly allowed.

2. Cross‑Origin Resource Sharing (CORS)

CORS provides a controlled way for a server to indicate which origins may access its resources. When a cross‑origin request is made, the browser may either allow it directly (simple request) or first send a preflight OPTIONS request.

The browser isolates content from different origins in separate processes; the network process fetches resources, while the rendering process may be blocked by CORB (Cross‑Origin Read Blocking) if the response contains potentially malicious data.

CORB prevents the rendering process from receiving cross‑origin data that could be exploited.

For a simple request, the following conditions must hold:

Only GET, HEAD, or POST methods are used.

Allowed request headers are limited to a whitelist (e.g., Accept, Content-Type with specific MIME types).

No ReadableStream objects are used.

No custom request headers are present.

No event listeners are attached to XMLHttpRequestUpload.

3. Preflight (OPTIONS) Request

If any condition above is violated, the browser sends a preflight request using the OPTIONS method. The preflight includes two special headers: Access-Control-Request-Method: the HTTP method the actual request intends to use (e.g., POST). Access-Control-Request-Headers: a comma‑separated list of custom headers the actual request will send (e.g., content-type,x-secsdk-csrf-token).

The server responds with headers such as: Access-Control-Allow-Origin: specifies an allowed origin (e.g., https://juejin.cn) or * for any origin. Access-Control-Allow-Methods: lists permitted HTTP methods (e.g., POST, GET). Access-Control-Allow-Headers: lists allowed custom headers. Access-Control-Max-Age: caches the preflight result (e.g., 86408 seconds ≈ 1 day).

After a successful preflight, subsequent actual requests include an Origin header, and the server must echo Access-Control-Allow-Origin for the browser to proceed.

4. Credentialed Requests and Wildcards

When cookies or other credentials are sent, the server must not use a wildcard ( *) for Access-Control-Allow-Origin; it must echo the exact requesting origin, otherwise the browser will reject the request. Similarly, Access-Control-Allow-Headers and Access-Control-Allow-Methods should list explicit values rather than * to avoid abuse.

5. Why Local Development with Webpack Doesn’t Need Server‑Side CORS

During local development, Webpack Dev Server acts as a proxy. The browser sends the request to the dev server, which forwards it to the target API according to the configured proxy rules, receives the response, and then returns it to the browser. Because the request never leaves the same origin (the dev server), the browser’s same‑origin check is bypassed, allowing seamless access to remote APIs without server‑side CORS configuration.

Summary

Preflight requests are automatic OPTIONS calls issued by browsers when a cross‑origin request does not qualify as a simple request. They protect users by letting the server explicitly approve the method, headers, and origin before the actual request is sent. Proper CORS headers—especially explicit origins and method lists—prevent credential leakage and mitigate security risks. Development tools like Webpack Dev Server can sidestep these restrictions by proxying requests, which is why local development often works without modifying server CORS settings.

CORSCross-OriginBrowser SecuritySame-Origin Policyweb securityPreflight Request
AI Illustrated Series
Written by

AI Illustrated Series

Illustrated hardcore tech: AI, agents, algorithms, databases—one picture worth a thousand words.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.