🧠 Understanding CORS, Preflight, and Custom Headers in API Requests
Adam C. |

Over the course of implementing stricter API protection for a project using custom headers, Cloudflare, and Feathers.js, I went deep into how CORS, preflight requests, and custom headers work — and where performance can unexpectedly take a hit.

This post summarizes what I learned.

Photo by Miguel Alcântara on Unsplash

✅ CORS and Preflight Basics

What triggers a preflight (OPTIONS) request?

Any non-simple request will trigger a preflight. In my case:

Cross-origin (main site calls a subdomain API)

Uses Content-Type: application/json

Uses custom headers like ss-cleanup or x-dnxclient-id

Therefore, every request results in:

An OPTIONS request

A full GET/POST request if preflight succeeds

What’s a “simple request” (that avoids preflight)?

Must satisfy:

Method: GET, POST, or HEAD

Content-Type: application/x-www-form-urlencoded, multipart/form-data, or text/plain

No custom headers

✅ Why My Requests Were Slower Than Expected

My headers looked like this:

headers: {
  "content-type": "application/json",
  "ss-cleanup": localStorage.getItem("SS-VENDOR_CLEANUP"),
  "x-dnxclient-id": generateNoise()
}

Which meant every fetch sent a preflight, costing ~100–300ms per request, even when calling between subdomains.

✅ Cloudflare Rule to Block Bots

To challenge non-browser bots, I used a custom header (ss-cleanup) that gets injected via real user interaction (mousedown, scroll, etc).

Cloudflare rule:

(starts_with(http.request.uri.path, "/dnxapi/meets")
 and not len(http.request.headers["ss-cleanup"]) > 0
 and http.request.method ne "OPTIONS")

✅ This works well:

Does not block browser preflight requests

Blocks or challenges bots skipping JS

Cleanly integrates with your frontend

✅ Safer CORS Setup in Feathers Backend (JUST IN CASE WE USE CORS)

const allowedOrigins = new Set([
  "https://example.com",
  "https://api.example.com",
  "https://community.example.com",
  "https://stg.example.com",
  "http://localhost:3000"
]);

app.use(cors({
  origin: function (origin, callback) {
    if (!origin || allowedOrigins.has(origin)) {
      callback(null, true);
    } else {
      callback(new Error("Not allowed by CORS"));
    }
  },
  credentials: true,
  allowedHeaders: [
    "Content-Type",
    "Authorization",
    "ss-cleanup",        // Custom validation header
    "x-dnxclient-id"     // Noise header to prevent silent failures
  ],
  maxAge: 600,             // Cache preflight result for 10 minutes
}));

✅ Better Architecture: Proxy API Through Same Domain

I migrated from:

example.com (Next.js)
  ↘ fetch https://api.example.com

To:

example.com (Next.js + Apache)
  ↘ proxy /dnxapi/* → http://localhost:8888/*

🔥 Key Wins:

No more CORS preflight

No DNS lookup for subdomain

No cross-origin cookie issues

Faster + cleaner setup

Cloudflare security rules still work

RewriteRule ^/dnxapi/(.*)$ http://localhost:8888/$1 [P,L]

📝 Notes & Final Takeaways

Access-Control-Max-Age is URL-specific

Preflight result is cached per exact URL. Even a minor query change busts the cache.

Changing to same-origin (via proxy) is the real fix

Removes CORS entirely – NO MORE PREFLIGHT!

No DNS lookup.

No cross-site concerns.

Feels much faster.