Serving Optimal Images with Content Negotiation - Accept Headers and CDN Integration
What Is Content Negotiation - Selecting Optimal Formats via HTTP
Content Negotiation is a standard HTTP protocol feature where client and server negotiate the optimal response format. For image delivery, the browser communicates supported image formats to the server, which responds with the most efficient format available.
How it works:
- Browser sends
Accept: image/avif,image/webp,image/png,image/*in request headers - Server parses the Accept header and checks format support in priority order
- Returns AVIF if supported, WebP if only WebP is supported, or JPEG/PNG as fallback
- Includes
Vary: Acceptheader in response to ensure cache correctness
Advantages: Unlike HTML <picture> element branching, formats can be switched without changing URLs. Existing <img src="photo.jpg"> tags remain unchanged while serving WebP or AVIF to supported browsers, requiring no HTML modifications.
Browser Accept header examples:
- Chrome 100+:
image/avif,image/webp,image/apng,image/*,*/*;q=0.8 - Firefox 93+:
image/avif,image/webp,*/* - Safari 16+:
image/webp,image/png,image/*;q=0.8,*/*;q=0.5
Safari added WebP support in iOS 16 / macOS Ventura (2023), and AVIF support from Safari 16.4 onwards.
Server-Side Implementation - Nginx and Apache Configuration Examples
Here's how to implement content negotiation on web servers, with configuration examples for both Nginx and Apache.
Nginx configuration:
map $http_accept $img_suffix { default ""; "~image/avif" ".avif"; "~image/webp" ".webp";}server { location ~* ^(.+)\.(jpe?g|png)$ { set $base $1; set $ext $2; add_header Vary Accept; try_files $base$img_suffix.$ext $uri =404; }}
This configuration returns the .avif version if image/avif is in the Accept header, the .webp version if image/webp is present, and falls back to the original JPEG/PNG if the alternative file doesn't exist.
Apache configuration (.htaccess):
RewriteEngine OnRewriteCond %{HTTP_ACCEPT} image/avifRewriteCond %{REQUEST_FILENAME}.avif -fRewriteRule ^(.+)\.(jpe?g|png)$ $1.$2.avif [T=image/avif,L]RewriteCond %{HTTP_ACCEPT} image/webpRewriteCond %{REQUEST_FILENAME}.webp -fRewriteRule ^(.+)\.(jpe?g|png)$ $1.$2.webp [T=image/webp,L]Header append Vary Accept
Critical note: Always include the Vary: Accept header. Without it, CDNs or proxies may cache the AVIF version and serve it to browsers that only support WebP, causing display failures.
Content Negotiation at the CDN Level - CloudFront and Cloudflare Setup
When using a CDN, implementing content negotiation at the CDN level is most efficient. Performing format detection at edge servers minimizes requests to the origin.
CloudFront configuration:
- Cache policy: Create a custom policy including the
Acceptheader in the cache key. However, Accept header values vary slightly between browsers, potentially reducing cache hit rates - Lambda@Edge: Normalize the Accept header in a Viewer Request trigger, consolidating into 3 patterns:
image/avif,image/webp,other. This significantly improves cache hit rates - CloudFront Functions: Lighter and faster than Lambda@Edge. Ideal for Accept header parsing and request URI rewriting
CloudFront Functions implementation:
function handler(event) { var request = event.request; var accept = request.headers.accept ? request.headers.accept.value : ''; var uri = request.uri; if (uri.match(/\.(jpe?g|png)$/i)) { if (accept.includes('image/avif')) { request.uri = uri + '.avif'; } else if (accept.includes('image/webp')) { request.uri = uri + '.webp'; } } return request;}
Cloudflare setup: Enabling Cloudflare's Polish feature performs automatic WebP conversion at the edge. Pro plan and above also offers AVIF conversion. Custom logic can also be implemented via Transform Rules.
Proper Vary Header Management - Preventing Cache Accidents
The Vary header is a critical element ensuring correct caching behavior for content negotiation. Misconfiguration can cause serious incidents where wrong formats are served from cache.
Role of the Vary header: Vary: Accept tells caches that "this response varies based on the Accept header value." CDNs and proxies use this information to maintain separate cache entries for each Accept header value.
Common accident patterns:
- No Vary: CDN caches the first request's (Chrome) AVIF response and serves AVIF to the next request (Safari) too, causing images not to display in Safari
- Vary: *: Don't cache if any request header differs. Effectively disables caching entirely, negating CDN benefits
- Vary: Accept-Encoding, Accept: Correct but may create too many cache variations, reducing hit rates
Accept header normalization: Since Accept header values differ slightly between browsers (presence of quality values, ordering differences), using them directly as cache keys creates separate caches for browsers supporting the same format. Normalizing Accept headers at the CDN edge to 3 values (avif, webp, default) maximizes cache hit rates.
Testing method: Use curl -H "Accept: image/webp" -I https://example.com/photo.jpg to verify response headers return Content-Type: image/webp and Vary: Accept. Make multiple requests with different Accept headers to confirm correct formats are returned.
Comparison with picture Element - Client-Side vs Server-Side Approaches
Image format switching can be done via HTML <picture> element (client-side) or content negotiation (server-side). Understanding each approach's characteristics enables appropriate selection.
picture element (client-side):
<picture> <source srcset="photo.avif" type="image/avif"> <source srcset="photo.webp" type="image/webp"> <img src="photo.jpg" alt="Photo"></picture>
Comparison:
- HTML changes: picture element requires modifying all page HTML. Content negotiation needs no HTML changes
- URL structure: picture element uses different URLs per format. Content negotiation uses a single URL
- Cache efficiency: picture element has independent caches per format. Content negotiation manages via Vary header
- SEO: picture element allows each URL to be independently indexed. Content negotiation manages under 1 URL
- Debugging: picture element is easily verified in browser DevTools. Content negotiation requires header inspection
Recommended usage:
- New projects:
<picture>element recommended. Explicit and predictable behavior - Improving existing projects: Content negotiation recommended. Deployable without HTML changes
- CMS / UGC: Content negotiation recommended. Cannot modify HTML for user-uploaded images
Troubleshooting and Monitoring - Production Operations
Content negotiation works transparently when configured correctly, but issues can arise from configuration errors or CDN behavior changes. Here are monitoring and troubleshooting techniques for production environments.
Common problems and solutions:
- Images not displaying: Bug in Accept header parsing logic returning unsupported formats. Simulate each browser's Accept header with curl to verify
- Low cache hit rate: Insufficient Accept header normalization. Check per-Vary hit rates in CDN cache statistics
- AVIF not being served: File doesn't exist or MIME type configuration missing. Verify
image/avifis registered in web server MIME types - File size increase: Rare cases where WebP/AVIF is larger than original JPEG. Add logic to compare sizes during conversion and use the smaller one
Monitoring implementation:
- Aggregate
Content-Typedistribution from CDN access logs to track AVIF/WebP delivery ratios - Measure image load times by format using Real User Monitoring (RUM)
- Automatically test correct image display across major browsers periodically (Playwright / Puppeteer)
Fallback reliability: The most critical requirement of content negotiation is reliably falling back to original images (JPEG/PNG) when no supported format is available. Always test that fallback paths remain intact when adding new formats. As an edge case, verify correct responses to requests with empty Accept headers (some bots and proxies).