MIME Type Configuration for Modern Media Servers

Correct Content-Type declarations are the foundation of every modern media pipeline. When a server sends the wrong MIME type — or omits it entirely — browsers fall back to binary sniffing, which blocks the render tree, forces redundant fallback downloads, and prevents hardware-accelerated decoder initialization. This guide is part of Core Media Fundamentals & Next-Gen Formats and covers every server platform in production use, from Nginx mime.types overrides to edge header injection in Cloudflare Workers. Paired with correct Cache-Control headers for image and video assets, MIME precision is the first step toward eliminating wasted bandwidth and improving LCP times by 15–30%.

How HTTP Content Negotiation Works for Media Assets

The browser and server negotiate which format to serve through a sequence of HTTP headers. The browser advertises its format capabilities in the Accept request header; the server must then respond with a matching Content-Type and, critically, a Vary: Accept response header so CDN edge nodes cache separate representations per capability tier.

The diagram below shows the full negotiation flow for a single image asset:

HTTP content negotiation flow for media assets Sequence diagram showing browser sending Accept header to origin/CDN, CDN checking cache keyed on Accept, origin serving AVIF or WebP or JPEG fallback, and CDN storing each variant separately under Vary: Accept. Browser CDN Edge Origin Server GET /hero.avif Accept: image/avif,image/webp,*/* Cache MISS → forward request Cache key includes Accept value 200 OK — Content-Type: image/avif Vary: Accept X-Content-Type-Options: nosniff Store variant keyed on Accept tier 200 OK — image/avif delivered Second request (same Accept) Cache HIT — served from edge No origin request needed

Three headers govern this exchange:

  • Content-Type — the authoritative MIME declaration the browser uses to select a decoder. Without it, Chrome attempts sniffing but Safari frequently refuses to decode.
  • Vary: Accept — tells every intermediate cache (CDN, reverse proxy, shared cache) to store separate responses for each distinct Accept value. Omitting this causes WebP-capable browsers to receive JPEG responses cached for older agents, or vice versa.
  • X-Content-Type-Options: nosniff — instructs browsers to honour the declared Content-Type and skip sniffing entirely. Required for security and for deterministic decoder routing.

Warning: Missing Vary: Accept causes CDN cache poisoning — a browser that does not support AVIF may receive a cached AVIF response, causing a blank image with no console error. Always verify the response header is present on every route that serves format-negotiated assets.

IANA MIME Types and Browser Support Matrix

MIME types for modern image and video formats are registered with IANA and must be used exactly as specified — servers that return image/x-avif or video/x-webm will cause negotiation failures in strict parsers. The AV1 video codec in particular requires a fully-qualified codec parameter string in the Content-Type when serving fragmented MP4 (fmp4) to MSE pipelines.

Format Correct IANA MIME Type Chrome / Edge Firefox Safari 14 Safari 16+
AVIF (still image) image/avif 85+ 93+ No 16+
WebP image/webp 23+ 65+ 14+ 14+
JPEG XL image/jxl Removed (v110) No 17+ 17+
AV1 in MP4 video/mp4; codecs="av01.0.05M.08" 70+ 67+ No 16.4+
VP9 in WebM video/webm; codecs="vp9" 32+ 28+ 14+ (limited) 14+ (limited)
H.265 in MP4 video/mp4; codecs="hvc1.1.6.L93.B0" 104+ (Win only) No 11+ 11+
H.264 in MP4 video/mp4; codecs="avc1.42E01E" 4+ 35+ 3.1+ 3.1+
DASH manifest application/dash+xml 27+ 47+ No No
HLS playlist application/vnd.apple.mpegurl Via MSE Via MSE 3+ (native) 3+ (native)

Quirk note: Safari requires exact codec string casing in both the HTTP Content-Type and the HTML <source type> attribute. A lowercase av01 works; AV01 does not. Always verify decoder readiness at runtime via HTMLMediaElement.canPlayType() before committing to a format variant — it returns "probably", "maybe", or "" (unsupported).

// Probe AV1 support before attempting to set a video src
const probe = document.createElement('video');
const av1Support = probe.canPlayType('video/mp4; codecs="av01.0.05M.08"');
// "probably" → safe to use AV1; "maybe" → server must send; "" → skip entirely
if (av1Support === 'probably' || av1Support === 'maybe') {
  videoEl.src = 'clip.av1.mp4';
} else {
  videoEl.src = 'clip.vp9.webm'; // VP9/WebM universal fallback
}

Step-by-Step Implementation

Step 1 — Nginx: Register types in mime.types

Nginx ships with a built-in mime.types file that predates AVIF and AV1. Override it with an explicit types {} block inside your http {} context (or inside a specific server {} block for single-vhost precision):

# /etc/nginx/conf.d/media-types.conf
# Place this inside the http{} block or in a dedicated include file.

types {
  # Modern image formats
  image/avif        avif;          # AVIF still image — RFC draft, IANA registered 2021
  image/webp        webp;          # WebP lossless/lossy
  image/jxl         jxl;          # JPEG XL — Safari 17+ only; ship with <picture> fallback

  # Video containers and codecs
  video/webm        webm;          # WebM container (VP8/VP9/AV1)
  video/mp4         mp4 m4v m4s;  # MP4 and MPEG-DASH segments (.m4s)
  video/ogg         ogv;           # Ogg/Theora — legacy only

  # Adaptive streaming manifests
  application/dash+xml          mpd;   # MPEG-DASH manifest
  application/vnd.apple.mpegurl m3u8;  # HLS playlist — required for Safari native HLS

  # Fonts (often missed, causes render-blocking if wrong)
  font/woff2  woff2;
  font/woff   woff;
}

# Force browsers to honour Content-Type; suppress sniffing sitewide
add_header X-Content-Type-Options "nosniff" always;

# Vary header for routes that serve format-negotiated images
# (set this in the location block that handles /images/ or your CDN origin path)
# add_header Vary "Accept" always;

Apply and test:

nginx -t && systemctl reload nginx
# Verify the response headers for an AVIF asset
curl -sI -H 'Accept: image/avif' https://your-cdn-origin.example/hero.avif \
  | grep -E 'content-type|vary|x-content-type'
# Expected output:
# content-type: image/avif
# vary: Accept
# x-content-type-options: nosniff

Step 2 — Apache: AddType directives and mod_headers

Apache’s default mime.types file (usually /etc/mime.types) is slow to adopt new format registrations. Add the missing types either in httpd.conf, a <VirtualHost> block, or a per-directory .htaccess:

# .htaccess or httpd.conf — modern media MIME types
# Requires: mod_mime, mod_headers

# Image formats
AddType image/avif  .avif
AddType image/webp  .webp
AddType image/jxl   .jxl

# Video formats
AddType video/webm  .webm
AddType video/mp4   .mp4 .m4v .m4s

# Adaptive streaming
AddType application/dash+xml          .mpd
AddType application/vnd.apple.mpegurl .m3u8

# Security: block MIME sniffing for all responses
Header always set X-Content-Type-Options "nosniff"

# Cache: send Vary on image routes so CDN caches per Accept tier
# Scope this to your image directory to avoid broad cache fragmentation

  Header always set Vary "Accept"

Tradeoff: Setting Vary: Accept globally on an Apache vhost fragments CDN cache across every Accept permutation, including Accept headers for HTML negotiation. Scope Vary: Accept only to image routes using <FilesMatch> or <Location> blocks to avoid unnecessary cache fragmentation on non-media responses.

Step 3 — Caddy: MIME types in Caddyfile

Caddy v2 handles many types automatically but does not register AVIF or AVIF sequences by default. Use the header directive:

# Caddyfile
your-origin.example {
  root * /var/www/media

  # Override Content-Type for modern image formats
  @avif path *.avif
  header @avif Content-Type "image/avif"
  header @avif X-Content-Type-Options "nosniff"
  header @avif Vary "Accept"

  @webp path *.webp
  header @webp Content-Type "image/webp"
  header @webp X-Content-Type-Options "nosniff"
  header @webp Vary "Accept"

  @webm path *.webm
  header @webm Content-Type "video/webm"
  header @webm X-Content-Type-Options "nosniff"

  file_server
}

Step 4 — Cloudflare Workers: Edge header injection

When the origin cannot be reconfigured (legacy CDN, third-party storage bucket, or a managed hosting platform), a Cloudflare Worker can inject correct headers at the edge. This approach also avoids adding a round-trip to origin for header fixes:

// worker.js — Cloudflare Worker (ES module syntax, wrangler.toml: main = "worker.js")
// Maps file extensions to authoritative MIME types.
// Runs at edge; no origin round-trip for MIME corrections.

const MIME_MAP = {
  '.avif': 'image/avif',
  '.webp': 'image/webp',
  '.jxl':  'image/jxl',
  '.webm': 'video/webm',
  '.mp4':  'video/mp4',
  '.m4s':  'video/mp4',         // DASH segments use mp4 container
  '.mpd':  'application/dash+xml',
  '.m3u8': 'application/vnd.apple.mpegurl',
};

const IMAGE_EXTS = new Set(['.avif', '.webp', '.jxl', '.jpg', '.jpeg', '.png']);

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    // Extract extension — pathname may include query strings from some origins
    const ext = url.pathname.match(/(\.[a-z0-9]+)$/i)?.[1]?.toLowerCase();

    const response = await fetch(request);
    const headers = new Headers(response.headers);

    if (ext && MIME_MAP[ext]) {
      headers.set('Content-Type', MIME_MAP[ext]);
    }

    // Prevent browser sniffing — must be on every response, not just matched ones
    headers.set('X-Content-Type-Options', 'nosniff');

    // Scope Vary: Accept to image routes only — prevents CDN cache fragmentation
    // on video and manifest responses where Accept doesn't drive format selection
    if (ext && IMAGE_EXTS.has(ext)) {
      headers.set('Vary', 'Accept');
    }

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

Warning: Using ES module export default syntax requires wrangler.toml to declare main = "worker.js" and compatibility_date of "2023-01-01" or later. The legacy addEventListener('fetch', ...) pattern does not support env bindings and should not be used for new Workers.

Step 5 — Content negotiation HTML patterns

Correct server MIME configuration enables the browser to honour <picture> source selection and <video> source ordering. The type attribute on each <source> is matched against the browser’s decoder registry — it is not just a hint:

<!-- Picture element: browser picks first <source> whose type it supports.
     Requires server to actually serve image/avif at hero.avif — mismatches cause blank images. -->
<picture>
  <source srcset="hero.avif" type="image/avif">
  <!-- WebP fallback for Safari 14 and Chrome < 85 -->
  <source srcset="hero.webp" type="image/webp">
  <!-- JPEG baseline — no type attribute needed; always supported -->
  <img src="hero.jpg" alt="Promotional banner: media delivery pipeline overview"
       loading="lazy"        <!-- below-fold: defer fetch -->
       decoding="async"      <!-- offload decode from main thread -->
       width="1200" height="630"> <!-- reserve layout space, eliminating CLS -->
</picture>

<!-- Video element: sources tried in order; first supported type wins.
     codec string must match exactly what the browser decoder registry expects. -->
<video controls
       preload="metadata"    <!-- fetch duration/dimensions; skip full download -->
       width="1280" height="720"
       poster="poster.webp"
       aria-label="Product demonstration: configuring MIME types on Nginx">
  <!-- AV1 in fMP4 — best compression, Chrome 70+, Firefox 67+, Safari 16.4+ -->
  <source src="clip.av1.mp4"  type='video/mp4; codecs="av01.0.05M.08"'>
  <!-- VP9 in WebM — universal modern fallback; Safari has limited support -->
  <source src="clip.vp9.webm" type='video/webm; codecs="vp9"'>
  <!-- H.264 baseline — maximum compatibility, including older Safari and Edge -->
  <source src="clip.h264.mp4" type='video/mp4; codecs="avc1.42E01E"'>
  <track kind="captions" src="captions.vtt" srclang="en" label="English">
  <p>HTML5 video is not supported. <a href="clip.h264.mp4">Download the video (MP4)</a>.</p>
</video>

Parameter Reference

Parameter / Header Where used Effect Common mistake
Content-Type: image/avif HTTP response header Routes to AVIF decoder Using image/x-avif — not IANA registered, rejected by strict parsers
Vary: Accept HTTP response header Keys CDN cache per Accept tier Omitting it — causes JPEG/AVIF cache collision on shared CDN nodes
X-Content-Type-Options: nosniff HTTP response header Disables browser sniffing Scoping to HTML responses only — must apply to all asset types
type='video/mp4; codecs="av01.0.05M.08"' <source> attribute Declares codec profile to decoder Wrong quantizer level in profile string causes canPlayType to return ""
type='video/webm; codecs="vp9"' <source> attribute Declares VP9 container/codec Omitting codec parameter — Safari may reject as unsupported even when WebM is available
preload="metadata" <video> attribute Fetches duration/dimensions without full download preload="auto" downloads the full video on page load — wasted bandwidth on mobile
loading="lazy" <img> attribute Defers off-screen image fetch Applying to LCP image — delays the critical image; use loading="eager" or omit for above-fold images
decoding="async" <img> attribute Offloads image decode from main thread No meaningful effect on loading="eager" images in the LCP path

Tradeoffs and Edge Cases

Tradeoff: JPEG XL delivery is Safari-only as of 2026. Chrome removed JPEG XL support in version 110 after a trial period. Firefox has not shipped it. Serving .jxl assets requires a robust <picture> fallback chain: JPEG XL → AVIF → WebP → JPEG. If your origin bucket stores only JPEG XL and JPEG, browsers without JXL support will fall through to JPEG even when WebP would offer better compression. Maintain all three next-gen variants in your encoding pipeline or use a CDN that transcodes on demand.

Tradeoff: Vary: Accept fragments CDN cache. Every distinct Accept header value creates a separate cache entry. Modern browsers send image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8 — a long, non-normalized string. Cloudflare normalizes Accept to a small set of tiers before using it as a cache key; AWS CloudFront does not normalize by default. On CloudFront, use Lambda@Edge or CloudFront Functions to normalize the Accept header to a three-value key (avif, webp, or baseline) before caching.

Tradeoff: AV1 codec profile strings are version-specific. The codec string av01.0.05M.08 encodes profile 0, level 5, tier Main, 8-bit depth. A 10-bit HDR encode requires av01.0.05M.10. Serving the wrong codec string in Content-Type causes canPlayType to return "probably" on the wrong variant, leading to decoder failures on devices without 10-bit support.

Tradeoff: HLS vs DASH MIME types must match the player library. video.js and hls.js key on application/vnd.apple.mpegurl for HLS and application/dash+xml for DASH. If the origin returns text/plain or application/octet-stream for .m3u8 files (common with misconfigured S3 buckets), adaptive bitrate players silently refuse to load the manifest. This is one of the most common failures when debugging incorrect Content-Type headers for WebM videos.

Tradeoff: X-Content-Type-Options: nosniff blocks cross-origin script loads. When the header is set on a JavaScript file that is loaded cross-origin and the Content-Type is anything other than a JavaScript MIME type, the browser blocks execution. Audit all cross-origin asset loads before enabling nosniff sitewide.

Debugging and Validation

CLI: verify headers with curl

# Check AVIF Content-Type, Vary, and nosniff on a production CDN node
curl -sI \
  -H 'Accept: image/avif,image/webp,*/*;q=0.8' \
  https://cdn.your-domain.com/images/hero.avif \
  | grep -iE 'content-type|vary|x-content-type|cache-status'

# Expected response headers:
# content-type: image/avif
# vary: Accept
# x-content-type-options: nosniff
# cache-status: HIT  (or cf-cache-status: HIT on Cloudflare)

# Check a WebM video response
curl -sI https://cdn.your-domain.com/videos/clip.vp9.webm \
  | grep -iE 'content-type|accept-ranges'
# Expected:
# content-type: video/webm
# accept-ranges: bytes   ← required for <video> seeking / range requests

Browser DevTools: Network panel waterfall

Open DevTools → Network → filter by Img or Media. Select the asset row and inspect the Response Headers panel:

  1. Confirm content-type matches the expected MIME type for the format that was served.
  2. Confirm vary: Accept is present on image responses.
  3. Check the Initiator tab — if the browser issued a second request for a fallback format, the first request returned a wrong or missing Content-Type.
  4. In the Timing tab, a non-zero Stalled duration combined with a content-type: application/octet-stream response indicates the browser initiated and then aborted a decode attempt.

Lighthouse and PerformanceResourceTiming

// In the browser console or a Lighthouse custom metric script:
// Compare transferSize vs decodedBodySize for image resources.
// A ratio close to 1 with zero retries confirms optimal MIME routing.

performance.getEntriesByType('resource')
  .filter(e => e.initiatorType === 'img' || e.initiatorType === 'video')
  .forEach(e => {
    const ratio = e.decodedBodySize / e.transferSize;
    console.log(`${e.name}: transfer=${e.transferSize}B decoded=${e.decodedBodySize}B ratio=${ratio.toFixed(2)}`);
    // ratio >> 1 = compressed (good); ratio ≈ 1 = uncompressed or wrong format served
  });

Run a full Lighthouse audit to quantify the LCP and CLS impact of MIME configuration changes:

# Lighthouse CLI — mobile simulation, performance category only
lighthouse https://your-domain.com \
  --only-categories=performance \
  --throttling-method=devtools \
  --output=json \
  --output-path=./lh-report.json

# Extract LCP and CLS from the JSON report
node -e "
  const r = require('./lh-report.json').audits;
  console.log('LCP:', r['largest-contentful-paint'].displayValue);
  console.log('CLS:', r['cumulative-layout-shift'].displayValue);
"

Monitor PerformanceResourceTiming.transferSize versus decodedBodySize in production via the Cache-Control and CDN caching documentation. A ratio near 1:1 with zero retried requests confirms correct MIME routing and efficient CDN cache hits.