๐ŸŒ Chapter 67: CORS & Cross-Origin Embedding

Simple vs Preflighted Requests

Understanding preflight `OPTIONS` handshakes, safe methods, forbidden custom headers, and the performance economics of cross-origin requests.

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 OPTIONS handshake headers (Access-Control-Request-Method, Access-Control-Request-Headers).
  • Optimize web performance and eliminate latency penalties using preflight caching (Access-Control-Max-Age).
๐ŸŽฌ INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
๐ŸŒ
1. Input
Directives & Tags
โš™๏ธ
2. Parse
Tokenizer & AST
๐ŸŒณ
3. Layout
Box Model & Flow
๐ŸŽจ
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

๐Ÿ“– 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:

  1. HTTP Method: Must be one of the three safelisted methods:
    • GET
    • HEAD
    • POST
  2. Request Headers: May only contain CORS-safelisted request headers:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (restricted to specific values below)
    • Range (simple range headers)
  3. Content-Type Restriction: If Content-Type is present, its value must be strictly one of:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  4. No ReadableStream upload: No streaming request body is used, and no XMLHttpRequestUpload event listeners (other than progress, 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-Type selection, and custom header checkboxes).
  • Lines 61โ€“64: Validates the HTTP method against the WHATWG safelist (GET, POST, HEAD). Methods like DELETE or PUT fail this test.
  • Lines 66โ€“69: Evaluates the Content-Type. If it is application/json, it triggers the preflight requirement.
  • Lines 71โ€“73: Checks for custom headers like Authorization or X-Api-Key.
  • Lines 84โ€“97: Displays the resulting network trace, explaining whether the browser sends one direct HTTP request or executes an advance OPTIONS handshake.

Expected Browser Render Output


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45ยฐC
INSPECTING DOM: VALID
TAGS: SCANNING...
โšก 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:

  1. Implement a server-side route handler simulation handlePreflightOptions(req) that receives an incoming OPTIONS request.
  2. Verify that Origin, Access-Control-Request-Method, and Access-Control-Request-Headers are present.
  3. Validate that the requested method is permitted (GET, POST, PUT, DELETE) and the requested headers are permitted (Content-Type, Authorization, X-Requested-With).
  4. If valid, return a 204 No Content response object containing:
    • Access-Control-Allow-Origin (reflected origin)
    • Access-Control-Allow-Methods
    • Access-Control-Allow-Headers
    • Access-Control-Max-Age: 86400
  5. If invalid (e.g. forbidden method requested), return a 403 Forbidden response.

๐Ÿ Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45ยฐC
INSPECTING DOM: VALID
TAGS: SCANNING...

โš ๏ธ Common Pitfalls

  1. Returning 401/403 on Preflight OPTIONS Due to Auth Middleware: Placing authentication middleware (e.g. JWT verification) before CORS middleware. The browser never sends Authorization credentials with the initial preflight OPTIONS request. If your server rejects unauthenticated OPTIONS requests with 401 Unauthorized, the browser will abort the real request before it ever starts!
  2. 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.
  3. Setting Access-Control-Max-Age Beyond Browser Limits: While servers can send Access-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

  1. Always Handle OPTIONS Fast at the Edge / Reverse Proxy: Configure Nginx, Envoy, or AWS CloudFront to handle and terminate OPTIONS preflight requests immediately with 204 No Content and static headers, preventing unnecessary loads on backend application servers.
  2. Use Safelisted Headers to Keep Requests Simple: If ultra-low latency is required (e.g. high-frequency telemetry beacons), use navigator.sendBeacon() or fetch() with text/plain or application/x-www-form-urlencoded payloads to keep requests classified as "Simple", eliminating the preflight round-trip entirely.

๐Ÿ“Œ Key Takeaways

  • Simple requests use GET, POST, or HEAD with 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 preflight OPTIONS handshake.
  • 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 OPTIONS request.
  • Use Access-Control-Max-Age to cache preflight decisions and eliminate redundant network round-trips.
  • --
โญ LEARN: HTML ๐ŸŒŸ โš”๏ธ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Which of the following fetch() calls will execute as a SIMPLE request without triggering a CORS preflight OPTIONS handshake?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

Why do backend servers often fail CORS preflights with a 401 Unauthorized error when authentication middleware is applied globally?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

What is the primary function of the Access-Control-Max-Age response header in a preflight response?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP