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. This guide is part of Image Optimization Pipelines for CMS Assets.
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:
/**
* 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
// Use a dedicated key parameter; never reuse names like `v` that version tokens already use.
const cacheKey = new URL(url.toString());
cacheKey.searchParams.set('__fmt', 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.
<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. Format choice should depend only on what the browser supports, never on the region: a bandwidth-constrained market benefits most from the smallest format the device can decode. 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: Accepton all origin image responses to prevent cache poisoning. - Normalize CDN cache keys with explicit format suffixes or query parameters.
- Deploy edge workers that parse
Acceptand 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.
Testing Negotiation
Negotiation bugs are invisible in a modern browser, which always receives the best format, so test with explicit headers. A small script requests each sample image three times with different Accept headers, one advertising AVIF, one WebP only and one */*, and asserts the Content-Type of each response. Run it against the CDN, not just the origin, and run it twice in a row: the second pass catches cache poisoning, where the first response for one header is served to the others. Add the same check to the post-deploy audit, and chart the distribution of image formats served from CDN logs, which reveals regressions such as a configuration change that silently disabled AVIF.
for accept in "image/avif,image/webp,*/*" "image/webp,*/*" "*/*"; do
curl -s -o /dev/null -w "%{content_type} $accept\n" -H "Accept: $accept" https://images.example.com/hero-42.jpg
done
Where Negotiation Should Live
There are three reasonable places to negotiate formats, and a pipeline should pick one. The image provider: many CMS image APIs and CDN image services accept an automatic format option, read the Accept header themselves and handle caching correctly; if yours does, use it and skip the worker. An edge worker: needed when the provider requires an explicit format parameter, as in the example above, or when you want control over the choice. The markup: picture elements with typed sources, where the browser chooses; best for static builds and when you want no header logic at all. Doing it in two places at once, for example a worker rewriting URLs that already request automatic format, leads to confusing results and doubled cache entries.
Gotchas & Edge Cases
- Accept headers that lie. Some clients advertise formats inconsistently, and crawlers often send
*/*. Treat anything without explicit AVIF or WebP support as JPEG-only. - Vary: Accept fragmentation. Varying on the raw
Acceptheader creates a cache entry per distinct header string. Normalize to the chosen format in the cache key, as the worker does, rather than relying onVaryalone at the CDN. - Downloads and social previews. Social crawlers may not support AVIF. Point
og:imageat a JPEG or PNG explicitly. - Transparency. JPEG has no alpha channel; the fallback for transparent images must be PNG, not JPEG.
Worked Example
A news site’s CDN cached the first format it saw for each image. Because a monitoring bot without AVIF support refreshed popular images every few minutes, most visitors received JPEGs. Adding the edge worker with a normalized format in the cache key raised the share of AVIF responses to match browser support, about 90 percent of requests, and reduced image bandwidth by roughly a third without any change to page markup.
Frequently Asked Questions
Is picture or Accept negotiation better?
Both work. picture is explicit and cache-friendly by URL; Accept negotiation keeps markup small. Many CMS image APIs offer an automatic format option that does the negotiation for you.
Do we still need JPEG fallbacks?
For a small share of browsers and many bots, yes. Keep them, generated on demand so they cost nothing until requested.
Should format be part of the URL?
With picture, yes. With Accept negotiation, the public URL stays the same, and the format goes into the internal cache key.
What about JPEG XL?
Browser support is limited. Treat it like any other optional format: offer it only to clients that advertise support, after AVIF or WebP.
How much does AVIF save compared with WebP?
Typically 20 to 30 percent at similar visual quality for photographs, less for graphics. Measure on your own images before tuning quality settings.
Does negotiation affect LCP?
Yes, positively: smaller files for the LCP image download faster. Make sure the LCP image’s format negotiation does not add a redirect or an extra request, since that delay would outweigh the saved bytes.
Can the CMS preview show modern formats?
Yes, if preview images go through the same pipeline. It is worth doing, so editors see the same compression artefacts, if any, that readers will.