demo · v141

Header Migration Guide

Chrome 141 stops sending the legacy Purpose: prefetch header and standardises on Sec-Purpose: prefetch. This guide shows the full timeline, server migration patterns, and a "Is my server ready?" code scanner — so nothing breaks after the update.

Header evolution timeline

Chrome < 103
Legacy header only
Chrome sent Purpose: prefetch on prefetch requests. Non-standard — no CORS alignment, no browser consensus.
Chrome 103–140
Both headers (transition period)
Chrome sent both Purpose: prefetch and the new Sec-Purpose: prefetch. Servers could migrate gradually while keeping compatibility.
Chrome 141+ (current)
Sec-Purpose only
Legacy Purpose: prefetch is removed. Only Sec-Purpose: prefetch is sent. Servers must be updated or they lose prefetch detection.
Future
Cross-browser standardisation
Firefox and Safari expected to adopt Sec-Purpose as part of navigation speculation spec standardisation.

Sec-Purpose values matrix

Sec-Purpose value Meaning Old Purpose header equivalent When Chrome sends it
prefetch Request is a prefetch — fetch-only, not rendered Purpose: prefetch Speculation rules with prefetch type; link rel=prefetch
prefetch;prerender Request is part of a prerender — full render, not yet activated (no equivalent — prerender is new) Speculation rules with prerender type; sub-resources of a prerender
(absent) Normal navigation or regular fetch (absent) All other requests
// ❌ OLD — breaks in Chrome 141
app.use((req, res, next) => {
  if (req.headers['purpose'] === 'prefetch') {
    // serve prefetch-optimized response
    res.setHeader('Cache-Control', 'no-store');
  }
  next();
});

// ✅ NEW — Chrome 141 compatible
app.use((req, res, next) => {
  const secPurpose = req.headers['sec-purpose'] || '';
  if (secPurpose === 'prefetch') {
    // prefetch request (fetch only, not yet rendered)
    res.setHeader('Cache-Control', 'no-store');
  } else if (secPurpose === 'prefetch;prerender') {
    // prerender request (full render, not yet activated)
    res.setHeader('Cache-Control', 'no-store');
  }
  next();
});

// ✅ TRANSITION — supports Chrome 103–140 AND Chrome 141+
app.use((req, res, next) => {
  const secPurpose = req.headers['sec-purpose'] || '';
  const legacyPurpose = req.headers['purpose'] || '';
  const isPrefetch = secPurpose.startsWith('prefetch') ||
                     legacyPurpose === 'prefetch';
  if (isPrefetch) {
    res.setHeader('Vary', 'Sec-Purpose, Purpose');
    res.setHeader('Cache-Control', 'no-store');
  }
  next();
});
# ❌ OLD — breaks in Chrome 141
if ($http_purpose = "prefetch") {
  add_header X-Prefetch-Mode "true";
}

# ✅ NEW — Chrome 141 compatible
if ($http_sec_purpose ~* "prefetch") {
  add_header X-Prefetch-Mode "true";
}

# For Vary header (cache keying):
# OLD:  Vary: Purpose
# NEW:  Vary: Sec-Purpose
add_header Vary "Sec-Purpose";

# TRANSITION (keep both during rollout):
map $http_sec_purpose $is_prefetch {
  ~prefetch  1;
  default    $http_purpose;  # fallback to legacy
}
// Cloudflare Workers / Deno Deploy / Vercel Edge

// ❌ OLD
export default {
  async fetch(request) {
    if (request.headers.get('Purpose') === 'prefetch') {
      return new Response('prefetch', { headers: { 'Cache-Control': 'no-store' } });
    }
    return fetch(request);
  }
};

// ✅ NEW — Chrome 141
export default {
  async fetch(request) {
    const secPurpose = request.headers.get('Sec-Purpose') ?? '';
    if (secPurpose.startsWith('prefetch')) {
      const response = await fetch(request);
      return new Response(response.body, {
        headers: {
          ...Object.fromEntries(response.headers),
          'Cache-Control': 'no-store',
          'Vary': 'Sec-Purpose',
        }
      });
    }
    return fetch(request);
  }
};

Choose a Chrome version and request type to see which headers would be present on the request, and which your server must look for.

Choose options and press "Simulate request".

Paste your server middleware or route handler. The checker looks for legacy Purpose header references and suggests migration.

Migration checklist

see also