Skip to content
Anti-Bot

What Is Header Order Fingerprinting? Why Every Request Kind Has Its Own Headers

Pim

Pim · Scrappey Research

October 28, 2026 4 min read

What Is Header Order Fingerprinting? Why Every Request Kind Has Its Own Headers — conceptual illustration
On this page

Header order fingerprinting checks whether the headers on a request, their order and their values match what a real browser sends for that kind of request. A browser does not have one header set. It builds the headers for each request from the request's destination and mode, so a page load, a stylesheet, an image, a fetch and a worker script all look different on the wire. An HTTP client that sends one copied header set on every request matches none of them.

Quick facts

Varies byRequest kind: navigation, iframe, script, stylesheet, image, fetch/XHR, worker, form POST
Headers that movesec-ch-ua*, Accept, Sec-Fetch-*, Priority, Origin, Content-Length
Chrome Accept valuesThree: documents, images, stylesheets; everything else sends */*
Priority headerRFC 9218; Chrome varies it per request and never sends it over HTTP/1.1
Measured onChrome 156 (Oct 2026) and iOS Safari 26, over HTTP/2 and HTTP/1.1

One browser, many header sets

A page load is not one request. It is a navigation, then stylesheets, scripts, images, fetches, maybe an iframe, a worker and a form submission. The Fetch standard gives each request a destination (document, script, image, empty for fetch) and a mode (navigate, no-cors, cors), and browsers derive the headers from those two values: which headers appear, in what order and with which values.

A server that terminates TLS sees all of it: the header list in wire order, over HTTP/2 the pseudo-header order and stream priority, over HTTP/1.1 the casing. A header set is therefore not a property of "Chrome" but of "Chrome loading an image" or "Chrome running a fetch from a worker". It sits one layer above HTTP/2 fingerprinting, which describes the connection, and JA4, which describes the TLS handshake.

What Chrome 156 sends per request kind

Measured against a header-logging server in October 2026:

Request kindOpening headersAcceptpriority
Typed navigationsec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, upgrade-insecure-requests, user-agentDocument list (with image/jxl and signed-exchange)u=0, i
Blocking scriptsec-ch-ua-platform, user-agent, sec-ch-ua, sec-ch-ua-mobile*/*u=1
StylesheetSubresource layouttext/css,*/*;q=0.1u=0
Page imageSubresource layoutImage list ending image/*,*/*;q=0.8u=2, i
fetch / XHRSubresource layout*/*u=1, i
Dedicated worker scriptaccept: */* first, user-agent after referer*/*u=4, i

Some details have fixed positions: content-length is the first header on every POST, origin is the first header on a CORS script, sec-fetch-user: ?1 appears only on user-activated navigations, and fetches made inside a dedicated worker carry no sec-ch-ua* client hints at all. Chrome omits any part of priority that equals the RFC 9218 default, so an async script sends no priority header.

Safari differs everywhere: no client hints, every GET starts with sec-fetch-dest, the tail is accept-language, priority, accept-encoding instead of Chrome's accept-encoding, accept-language, priority, and every request states an explicit urgency.

The mistakes HTTP clients make

  • One header set for everything. A captured navigation (sec-fetch-dest: document, sec-fetch-user: ?1, document Accept, priority: u=0, i) sent on every request, including ones that claim to be an XHR or an image.
  • Fetch Metadata that contradicts the request, such as sec-fetch-mode: navigate on a POST with an Origin and a JSON body.
  • A priority header on HTTP/1.1 from a client that claims to be Chrome, or a priority that never changes.
  • Uniform casing on HTTP/1.1. Chrome mixes Title-Case standard headers with lowercase client hints; all-lowercase and all-Title-Case are both wrong.
  • Stale values. A document Accept without image/jxl no longer matches Chrome 156.

Libraries that impersonate TLS and HTTP/2 well, such as curl_cffi, usually leave per-request headers to the caller. Matching a browser means a table per request kind that tracks each Chrome release, not one profile. A real browser gets this right without a table because it builds the headers itself, which is why a managed web data API that renders pages in a real browser does not need to maintain one.

Code example

javascript
// Log the exact header order your client sends, per request.
// Run: node headers.mjs, then point a browser or your HTTP client at :8080.
// HTTP/1.1 only; HTTP/2 needs a TLS server (node:http2 with a certificate).
import http from 'node:http';

const page = `<!doctype html>
<link rel="stylesheet" href="/style.css">
<script src="/app.js"></script>
<img src="/pixel.png">
<script>fetch('/api')</script>`;

http.createServer((req, res) => {
  // rawHeaders keeps wire order and casing: [name, value, name, value, ...]
  const names = req.rawHeaders.filter((_, i) => i % 2 === 0);
  console.log(req.method, req.url.padEnd(12), names.join(', '));
  if (req.url === '/') { res.setHeader('content-type', 'text/html'); return res.end(page); }
  res.end('');
}).listen(8080);

// Compare the line for "/" with the lines for /style.css, /app.js, /pixel.png
// and /api. A browser prints five different header lists; a client that
// reuses one header dict prints the same list five times.

Related terms

Concept map

Concept map

How Header Order Fingerprinting connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

Does header order matter over HTTP/2, where header names are lowercase?

Yes. HTTP/2 lowercases names, but the order of regular headers and pseudo-headers is still preserved on the wire and visible to the server. Casing only adds a second signal on HTTP/1.1.

Is copying the headers from DevTools enough?

Only for the one request you copied. DevTools shows a navigation with navigation headers; using that set for an API call or an image gives the wrong Accept, the wrong Sec-Fetch values and the wrong priority. You need one set per request kind, and the sets change with Chrome releases.

Why do requests from a web worker have no sec-ch-ua headers?

In Chrome 156, fetches made inside a dedicated worker carry no client hints at all, and the worker script itself is fetched with a layout that starts with accept: */*. A client that adds client hints to everything looks wrong on exactly those requests.

Last updated: 2026-10-28 · Facts last verified: 2026-10-28

\n\n`;\n\nhttp.createServer((req, res) => {\n // rawHeaders keeps wire order and casing: [name, value, name, value, ...]\n const names = req.rawHeaders.filter((_, i) => i % 2 === 0);\n console.log(req.method, req.url.padEnd(12), names.join(', '));\n if (req.url === '/') { res.setHeader('content-type', 'text/html'); return res.end(page); }\n res.end('');\n}).listen(8080);\n\n// Compare the line for \"/\" with the lines for /style.css, /app.js, /pixel.png\n// and /api. A browser prints five different header lists; a client that\n// reuses one header dict prints the same list five times."},"relatedSlugs":["what-is-http2-fingerprinting","what-is-client-hints-fingerprinting","what-is-ja4-fingerprinting","how-websites-detect-scrapers","what-is-curl-cffi","what-is-sec-ch-ua-full-version-list"],"faq":[{"q":"Does header order matter over HTTP/2, where header names are lowercase?","a":"Yes. HTTP/2 lowercases names, but the order of regular headers and pseudo-headers is still preserved on the wire and visible to the server. Casing only adds a second signal on HTTP/1.1."},{"q":"Is copying the headers from DevTools enough?","a":"Only for the one request you copied. DevTools shows a navigation with navigation headers; using that set for an API call or an image gives the wrong Accept, the wrong Sec-Fetch values and the wrong priority. You need one set per request kind, and the sets change with Chrome releases."},{"q":"Why do requests from a web worker have no sec-ch-ua headers?","a":"In Chrome 156, fetches made inside a dedicated worker carry no client hints at all, and the worker script itself is fetched with a layout that starts with accept: */*. A client that adds client hints to everything looks wrong on exactly those requests."}],"updatedAt":"2026-10-28","verifiedAt":"2026-10-28"}}