Handling image format negotiation in headless pipelines

Image format negotiation breaks in headless pipelines because most content APIs are stateless data endpoints that never inspect the Accept header. Without explicit negotiation logic, the CDN caches one default format and serves it to everyone, inflating payloads and fragmenting the edge cache. Getting it right means coordinating header handling across the content layer, edge compute, and frontend markup.

The Content Negotiation Handshake

Browsers advertise codec support through the Accept request header. A current Chromium client sends image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8; older agents fall back to image/jpeg or image/png. Headless content APIs typically ignore this header and rely on explicit URL parameters or a downstream edge worker to pick the variant.

The common production failure is header stripping: reverse proxies and API gateways drop or normalize Accept before it reaches the origin. The origin then caches a single default format and serves it indiscriminately — silent format regression across every device segment, with the bandwidth bill to match.

Cache Keys and the Vary Header

Edge caches key on the request URI. Without Vary: Accept on origin responses, the first cached variant becomes the canonical response for all clients, which breaks HTTP content negotiation outright.

The failure looks like this: a legacy crawler requests the JPEG variant first, the edge caches it, and modern browsers asking for AVIF get the stale JPEG — negating every byte you saved. Fix it on two sides: append a format suffix to the CDN cache key, and set Vary: Accept at the origin. The relevant directives are in the HTTP Caching specification.

Edge Worker and Fallback Chain

Enforce a deterministic resolution chain at the edge: AVIF, then WebP, then JPEG/PNG. The worker parses Accept, rewrites the asset URL, and normalizes the cache key before the request hits the origin or transformation service.

The worker’s resolution-and-cache path looks like this:

flowchart TD
  A["Request with Accept header"] --> B{"Accept includes image/avif?"}
  B -->|yes| F1["format = avif"]
  B -->|no| C{"Accept includes image/webp?"}
  C -->|yes| F2["format = webp"]
  C -->|no| F3["format = jpeg"]
  F1 --> K["Normalize cache key (?v=format)"]
  F2 --> K
  F3 --> K
  K --> M{"Cache hit?"}
  M -->|yes| R["Return cached variant"]
  M -->|no| O["Fetch origin with ?fm=format"]
  O --> V["Set Vary: Accept + immutable Cache-Control"]
  V --> P["Store in edge cache"]
  P --> R
TypeScript
/**
 * Edge worker for deterministic image format negotiation.
 * Compatible with Cloudflare Workers, Vercel Edge, and Deno Deploy.
 */
export interface Env {
  IMAGE_ORIGIN: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const acceptHeader = request.headers.get('accept') || '';
    
    // Deterministic format resolution chain
    const format = acceptHeader.includes('image/avif') ? 'avif'
      : acceptHeader.includes('image/webp') ? 'webp'
      : 'jpeg';

    // Rewrite URL with explicit format parameter for origin processing
    url.searchParams.set('fm', format);

    // Normalize cache key to prevent cross-format leakage
    const cacheKey = new URL(url.toString());
    cacheKey.searchParams.set('v', format);

    const cache = caches.default;
    let response = await cache.match(cacheKey);

    if (!response) {
      response = await fetch(url.toString(), { headers: request.headers });
      
      // Enforce Vary header and cache control
      const headers = new Headers(response.headers);
      headers.set('Vary', 'Accept');
      headers.set('Cache-Control', 'public, max-age=31536000, immutable');
      headers.set('X-Format-Negotiated', format);

      response = new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers
      });

      await cache.put(cacheKey, response.clone());
    }

    return response;
  }
};

The format-suffixed cache key keeps variants from leaking across clients, and Vary: Accept documents the negotiation in the response.

SSG/SSR: Defer the Final Choice to the Browser

Static generation pre-renders markup at build time, where there is no browser context and no Accept header to read. Hardcoding one format at build forces legacy devices to decode codecs they don’t support.

Push the final choice back to the browser with a <picture> element. The browser walks the <source> list in order and takes the first supported type, so negotiation happens client-side with no build-time assumptions.

HTML
<picture>
  <source srcset="/assets/hero.avif" type="image/avif">
  <source srcset="/assets/hero.webp" type="image/webp">
  <img src="/assets/hero.jpg" alt="Hero banner" width="1200" height="630" loading="eager">
</picture>

The <picture> element and its decoding rules are specified in the WHATWG HTML Living Standard.

Localization and Asset Routing

Locale prefixes, regional CDN nodes, and localized metadata can all disrupt delivery if negotiation isn’t applied uniformly. Edge workers must respect locale prefixes while keeping format resolution identical across regions. Regional fallback chains can differ — a European node may default to AVIF, a node in a bandwidth-constrained market to WebP — so fold these rules into your broader Image Optimization Pipelines for CMS Assets.

Define fallback behavior explicitly for missing localized variants: resolve to the default locale’s optimized binary rather than 404ing or regressing the format. Deterministic edge routing keeps content fallback and asset delivery in step.

Production Checklist

  • Set Vary: Accept on all origin image responses to prevent cache poisoning.
  • Normalize CDN cache keys with explicit format suffixes or query parameters.
  • Deploy edge workers that parse Accept and rewrite URLs deterministically.
  • Use <picture> in SSG/SSR templates to delegate codec selection to the browser.
  • Sync locale-specific routing with regional CDN fallback chains.
  • Monitor LCP, cache hit ratio, and format distribution to catch silent regression.