demo · v138

Header trace

Three different ways to trigger a fetch — <link rel="prefetch">, speculation-rules prefetch, plain fetch() — sent through a Service Worker that records the exact request headers. The compatibility matrix shows what every browser does, with v138's addition highlighted.

Unflagged in Chrome 138. link rel="prefetch" now sends Sec-Purpose: prefetch. Brings it in line with speculation-rules prefetch (which already did) so origin servers and analytics platforms can branch on a single header.
probing…

Compatibility matrix

request mechanism
Sec-Purpose: prefetch
Sec-Fetch-Dest
Purpose: prefetch (legacy)
<link rel="prefetch">
v138 NEW — yes
empty
legacy yes (until removal)
<script type=speculationrules> prefetch
yes (since v109)
empty
no
<script type=speculationrules> prerender
yes: prefetch;prerender
document
no
plain fetch()
no
empty
no

Request traces

Server-side branching

// e.g. Express / Cloudflare Worker
app.get('/article/:id', (req, res) => {
  const isSpeculative = req.headers['sec-purpose'] === 'prefetch';

  if (isSpeculative) {
    // skip analytics, skip 'mark as read', skip rate limit increments
    return renderArticle(req, res, {trackHit: false, useCachedSession: true});
  }
  return renderArticle(req, res, {trackHit: true});
});

What's happening

  1. Pre-v138, only speculation-rules prefetches sent Sec-Purpose. link rel="prefetch" sent the older Purpose: prefetch instead — different name, different value space.
  2. v138 unifies them: link rel="prefetch" now also sends Sec-Purpose: prefetch. (The legacy header is still sent for backward compat.)
  3. Origin servers and SWs can now branch on a single canonical header. The tracer above shows it live: trigger each mechanism, watch the recorded headers.

see also