LEARNING OBJECTIVES โต
- Classify any HTTP request as either a Simple Request or a Preflighted Request based on the WHATWG Fetch specification.
- Understand why preflight checks exist to protect legacy servers from non-browser-safe HTTP methods.
- Dissect the preflight
OPTIONShandshake headers (Access-Control-Request-Method,Access-Control-Request-Headers). - Optimize web performance and eliminate latency penalties using preflight caching (
Access-Control-Max-Age).
๐ The Mental Model & Story (Intuitive Foundation)
Imagine sending a package containing delicate or potentially dangerous chemicals to an international research facility.
+-----------------------------------------------------------------------------------------------+
| SIMPLE VS PREFLIGHT ANALOGY |
+-----------------------------------------------------------------------------------------------+
| CASE 1: SIMPLE REQUEST (Standard Postcard / Simple Letter) |
| [Client] ================== Standard Mail (GET / POST Form) ===================> [Server] |
| * The letter is delivered directly. The server opens and reads it immediately. |
| |
| CASE 2: PREFLIGHTED REQUEST (Heavy Machinery / Hazardous Delivery) |
| Step 1: [Courier] === "Do you accept hazardous chemical containers?" (OPTIONS) => [Server] |
| Step 2: [Server] === "Yes, we accept chemicals from Lab Alpha on Dock 4." =====> [Courier] |
| Step 3: [Client] === Actual Tanker Delivery (PUT / JSON Payload) =============> [Server] |
+-----------------------------------------------------------------------------------------------+
If you send a standard postcard (a simple GET or standard HTML POST form), the post office delivers it straight to the destination box. Legacy mail systems have processed standard letters since the dawn of the postal service.
However, if you want to deliver specialized industrial machinery requiring a crane and custom hazardous disposal protocols (e.g., a DELETE request or a custom Content-Type: application/json payload with custom security tokens), sending the tanker truck unannounced could destroy a legacy loading dock!
To protect servers that were built before CORS existed (which never anticipated non-browser agents sending DELETE or PUT from arbitrary web pages), the browser sends an advance scout first: an HTTP OPTIONS Preflight Request. The scout asks: "Are you equipped and willing to accept a DELETE request with custom headers from origin X?" Only after the server returns an explicit green light does the browser dispatch the actual payload.
Technical Deep Dive & Specifications
The Strict Criteria for a "Simple Request"
According to the WHATWG Fetch Living Standard, a cross-origin request qualifies as a Simple Request (and avoids the preflight OPTIONS round-trip) only if ALL of the following four conditions are met:
- HTTP Method: Must be one of the three safelisted methods:
GETHEADPOST
- Request Headers: May only contain CORS-safelisted request headers:
AcceptAccept-LanguageContent-LanguageContent-Type(restricted to specific values below)Range(simple range headers)
Content-TypeRestriction: IfContent-Typeis present, its value must be strictly one of:application/x-www-form-urlencodedmultipart/form-datatext/plain
- No
ReadableStreamupload: No streaming request body is used, and noXMLHttpRequestUploadevent listeners (other thanprogress,load, etc.) are attached.
+------------------------------------------------------------------------------------+
| ๐จ CRITICAL NOTICE: application/json IS NOT A SIMPLE CONTENT-TYPE! |
| |
| Sending fetch('https://api.com', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, // <-- TRIGGERS PREFLIGHT! |
| body: JSON.stringify({ user: "Alice" }) |
| }); |
+------------------------------------------------------------------------------------+
Preflight Request & Response Handshake Anatomy
When any condition above is violated (e.g., method is PUT, DELETE, PATCH, or custom headers like Authorization: Bearer ... or X-Api-Key are included), the browser executes the two-phase Preflight Handshake:
+------------------------------------------------------------------------------------+
| THE PREFLIGHT OPTIONS HANDSHAKE |
+------------------------------------------------------------------------------------+
| PHASE 1: PREFLIGHT PROBE |
| Client Browser -------------------------------------------------> Origin Server |
| OPTIONS /v2/records/89 HTTP/1.1 |
| Host: api.example.com |
| Origin: https://frontend.app.com |
| Access-Control-Request-Method: DELETE |
| Access-Control-Request-Headers: authorization, content-type |
| |
| Origin Server --------------------------------------------------> Client Browser |
| HTTP/1.1 204 No Content (or 200 OK) |
| Access-Control-Allow-Origin: https://frontend.app.com |
| Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS |
| Access-Control-Allow-Headers: Authorization, Content-Type |
| Access-Control-Max-Age: 86400 <-- (Cache preflight approval for 24 hours) |
| |
| PHASE 2: THE ACTUAL DISPATCH (Only executed if Phase 1 succeeds!) |
| Client Browser -------------------------------------------------> Origin Server |
| DELETE /v2/records/89 HTTP/1.1 |
| Host: api.example.com |
| Origin: https://frontend.app.com |
| Authorization: Bearer eyJhbGciOi... |
| |
| Origin Server --------------------------------------------------> Client Browser |
| HTTP/1.1 200 OK |
| {"deleted": true, "recordId": 89} |
+------------------------------------------------------------------------------------+
Preflight Decision Matrix
| Request Characteristics | Simple or Preflighted? | Triggering Factor |
|---|---|---|
GET /feed (Default headers) |
๐ข Simple | Safelisted method and default browser headers. |
POST /submit (Content-Type: text/plain) |
๐ข Simple | Safelisted method and safelisted text Content-Type. |
POST /upload (Content-Type: multipart/form-data) |
๐ข Simple | Standard HTML form file upload format. |
POST /api/user (Content-Type: application/json) |
๐ด Preflighted | application/json is not in the CORS-safelisted MIME list. |
PUT /items/1 (Content-Type: text/plain) |
๐ด Preflighted | PUT is not a safelisted method. |
DELETE /items/1 |
๐ด Preflighted | DELETE is not a safelisted method. |
GET /profile with Authorization: Bearer token |
๐ด Preflighted | Authorization is a custom, non-safelisted request header. |
GET /search with X-Trace-ID: 98124 |
๐ด Preflighted | Custom header X-Trace-ID requires preflight approval. |
๐ป Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Lines 50โ54: Reads user inputs (HTTP method,
Content-Typeselection, and custom header checkboxes). - Lines 61โ64: Validates the HTTP method against the WHATWG safelist (
GET,POST,HEAD). Methods likeDELETEorPUTfail this test. - Lines 66โ69: Evaluates the
Content-Type. If it isapplication/json, it triggers the preflight requirement. - Lines 71โ73: Checks for custom headers like
AuthorizationorX-Api-Key. - Lines 84โ97: Displays the resulting network trace, explaining whether the browser sends one direct HTTP request or executes an advance
OPTIONShandshake.
Expected Browser Render Output
โก CORS Preflight Classifier & Network Inspector
[ HTTP Method: DELETE ]
[ Content-Type: application/json ]
[ [x] Authorization: Bearer ... ]
[ Analyze Network Request Trajectory ]
[ Output Box - Red Background ]:
๐ด CLASSIFICATION: PREFLIGHTED REQUEST (2 Network Round-Trips)
Preflight Trigger Factors:
โข Method 'DELETE' is not a CORS-safelisted method (GET, POST, HEAD).
โข Content-Type 'application/json' is not a CORS-safelisted MIME type.
โข Custom headers [Authorization] are not CORS-safelisted.
Execution Flow:
1. Browser issues: OPTIONS /endpoint HTTP/1.1
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: content-type, authorization
2. Server must respond with HTTP 204 No Content + Access-Control-Allow-Methods/Headers.
3. Browser only dispatches actual DELETE request if preflight succeeds!๐๏ธ Hands-On Exercise
๐ฏ The Challenge: Build a Complete Preflight Response Generator
Instructions:
- Implement a server-side route handler simulation
handlePreflightOptions(req)that receives an incomingOPTIONSrequest. - Verify that
Origin,Access-Control-Request-Method, andAccess-Control-Request-Headersare present. - Validate that the requested method is permitted (
GET, POST, PUT, DELETE) and the requested headers are permitted (Content-Type, Authorization, X-Requested-With). - If valid, return a
204 No Contentresponse object containing:Access-Control-Allow-Origin(reflected origin)Access-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Max-Age: 86400
- If invalid (e.g. forbidden method requested), return a
403 Forbiddenresponse.
๐ Starter Code Sandbox
โ ๏ธ Common Pitfalls
- Returning 401/403 on Preflight
OPTIONSDue to Auth Middleware: Placing authentication middleware (e.g. JWT verification) before CORS middleware. The browser never sendsAuthorizationcredentials with the initial preflightOPTIONSrequest. If your server rejects unauthenticatedOPTIONSrequests with401 Unauthorized, the browser will abort the real request before it ever starts! - Ignoring the Preflight Latency Tax: Every preflight request introduces an extra network round-trip. On mobile 4G networks with 150ms RTT, an uncached preflight doubles API response time from 150ms to 300ms.
- Setting
Access-Control-Max-AgeBeyond Browser Limits: While servers can sendAccess-Control-Max-Age: 31536000, Chrome caps the maximum cache duration to 7200 seconds (2 hours) and Firefox caps it to 86400 seconds (24 hours) to balance performance with security freshness.
๐ก Pro Tips
- Always Handle
OPTIONSFast at the Edge / Reverse Proxy: Configure Nginx, Envoy, or AWS CloudFront to handle and terminateOPTIONSpreflight requests immediately with204 No Contentand static headers, preventing unnecessary loads on backend application servers. - Use Safelisted Headers to Keep Requests Simple: If ultra-low latency is required (e.g. high-frequency telemetry beacons), use
navigator.sendBeacon()orfetch()withtext/plainorapplication/x-www-form-urlencodedpayloads to keep requests classified as "Simple", eliminating the preflight round-trip entirely.
๐ Key Takeaways
- Simple requests use
GET,POST, orHEADwith safelisted headers and safe MIME types (text/plain,x-www-form-urlencoded,multipart/form-data). - Requests using
application/json,PUT,DELETE,PATCH, or custom headers (Authorization,X-Api-Key) always trigger a preflightOPTIONShandshake. - Preflight requests exist to protect legacy backend servers from unexpected HTTP verbs sent by cross-origin web scripts.
- The browser never attaches authentication tokens or cookies to the preflight
OPTIONSrequest. - Use
Access-Control-Max-Ageto cache preflight decisions and eliminate redundant network round-trips. - --