Configuring the AVIF MIME type on Apache and Caddy
An AVIF file served with the wrong Content-Type is a broken image with no error message. Both Apache and Caddy ship default MIME tables that predate AVIF, so a freshly-deployed .avif often goes out as application/octet-stream — and the browser, seeing “arbitrary bytes,” refuses to route it to the image decoder. This guide is part of MIME Type Configuration for Modern Media Servers within Core Media Fundamentals & Next-Gen Formats, and gives you the exact one-line-per-format registration for Apache (AddType) and Caddy (header / mime), plus the curl checks that prove it worked.
Why the default is wrong
Apache resolves Content-Type from mod_mime, which reads a mime.types database (typically /etc/mime.types) mapping extensions to types. That database is conservative and slow to add new registrations, so .avif frequently has no entry — and when mod_mime finds no mapping, it falls back to DefaultType, which on modern Apache means no Content-Type header at all, or application/octet-stream if one is forced upstream.
Caddy v2 has an internal MIME table and sets many types automatically from the file extension, but AVIF (and AVIF image sequences) are not always covered depending on version, so you may still see application/octet-stream. The fix on both servers is to declare the type explicitly for the extensions you serve.
Warning: image/avif is the correct, IANA-registered type. Do not use image/x-avif — the x- prefix denotes an unregistered experimental type, and strict parsers (and some CDNs) will reject or mis-handle it, leaving you with the same blank-image failure you were trying to fix.
Prerequisite checklist
Step 1 — Confirm the problem
# Baseline: what Content-Type is the server sending today?
curl -sI https://example.com/img/hero.avif | grep -i content-type
# The failure signature you're fixing:
# content-type: application/octet-stream ← browser will NOT decode this as an image
# (or no content-type line at all)
Step 2 — Register on Apache
Add the types in the narrowest scope that works for you: a <VirtualHost> block (server-wide, no per-request cost), or an .htaccess in the media directory (convenient on shared hosting, but read on every request).
# .htaccess or inside <VirtualHost> — modern media MIME registration
# Requires: mod_mime (default). mod_headers only needed for the Vary/nosniff lines.
# Image formats — the core fix
AddType image/avif .avif
AddType image/webp .webp
# Video containers, commonly missing alongside AVIF
AddType video/webm .webm
AddType video/mp4 .mp4 .m4v
<IfModule mod_headers.c>
# Force browsers to honour the declared type instead of sniffing.
Header always set X-Content-Type-Options "nosniff"
# Scope Vary: Accept to image routes ONLY. Setting it globally fragments the CDN
# cache across every Accept permutation, including HTML negotiation — a real hit-rate loss.
<FilesMatch "\.(avif|webp|jpg|jpeg|png)$">
Header always set Vary "Accept"
</FilesMatch>
</IfModule>
Reload and re-check:
# Test config, then reload (Debian/Ubuntu shown; use httpd on RHEL)
apache2ctl configtest && systemctl reload apache2
curl -sI https://example.com/img/hero.avif | grep -i content-type
# content-type: image/avif ← fixed
Tradeoff: .htaccess is convenient because it needs no server reload, but Apache reads and parses it on every request to that directory tree, which adds measurable latency under load. For production, move AddType directives into the vhost and disable .htaccess (AllowOverride None) on the media path — you trade a little deploy convenience for lower per-request overhead.
Warning: If AllowOverride does not include FileInfo (or All) for the directory, your .htaccess AddType lines are silently ignored — the file parses but the directives have no effect. Confirm the <Directory> block permits FileInfo overrides, or the type will stay octet-stream no matter what you add.
Step 3 — Register on Caddy
Caddy offers two idioms. The header directive overrides the Content-Type explicitly per path matcher — precise and self-documenting. The mime directive maps extensions to types more declaratively. Prefer header when you also want to set Vary/nosniff on the same matcher.
# Caddyfile — register modern media types via explicit header overrides
example.com {
root * /var/www/media
# AVIF: override Content-Type and pin the negotiation/security headers together
@avif path *.avif
header @avif Content-Type "image/avif"
header @avif X-Content-Type-Options "nosniff"
header @avif Vary "Accept" # scope to image matcher, not site-wide
@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" # video: no Vary: Accept — source order, not negotiation
file_server
}
If you prefer the declarative mime directive (Caddy maps extension → type, and file_server uses it), it is terser but does not carry the extra headers:
example.com {
root * /var/www/media
# mime maps extensions to Content-Type; no Vary/nosniff here, add via header if needed
mime {
.avif image/avif
.webp image/webp
.webm video/webm
}
file_server
}
Reload and verify:
caddy reload --config /etc/caddy/Caddyfile
curl -sI https://example.com/img/hero.avif | grep -iE 'content-type|vary|x-content-type'
# content-type: image/avif
# vary: Accept
# x-content-type-options: nosniff
Tradeoff: The header directive sets the response header regardless of what file_server would infer, so it always wins — but if you have overlapping matchers it can double-set the type. The mime directive integrates with file_server’s own type detection and is cleaner for a pure extension→type map. Pick one idiom per path; mixing them on the same matcher makes the effective Content-Type harder to reason about.
Step 4 — Verify decode end to end
Header correctness is necessary but not sufficient — confirm the browser actually decodes it:
# Simulate an AVIF-capable client and confirm the negotiated type comes back correct
curl -sI -H 'Accept: image/avif,image/webp,*/*;q=0.8' \
https://example.com/img/hero.avif \
| grep -iE 'content-type|vary'
# content-type: image/avif
# vary: Accept
Then load the page in Chrome DevTools → Network, click the AVIF row, and confirm the Response Headers show content-type: image/avif and the image renders. A second request in the Initiator tab for a fallback format is the tell-tale sign the first request returned the wrong type — the browser fetched, failed to decode, and fell through your <picture> chain.
Apache vs Caddy at a glance
Common mistakes and fixes
1. Using image/x-avif
Anti-pattern: AddType image/x-avif .avif (or the Caddy equivalent).
Effect: The x- prefix marks an unregistered type. Some browsers tolerate it, but strict parsers and certain CDNs reject it, so the image intermittently fails to render — a maddening, environment-dependent bug.
Fix: Always use exactly image/avif. Confirm with curl -sI.
2. Leaving the application/octet-stream fallback in place
Anti-pattern: Adding AddType image/avif .avif but a broader rule still forces application/octet-stream for unknown/binary paths that also match .avif.
Effect: The more specific octet-stream rule can win, and the browser still gets binary. This is common behind download-oriented <Location> blocks or S3-origin defaults.
Fix: Ensure no upstream directive re-sets Content-Type for the media path. On Apache, a <FilesMatch "\.avif$"> Header set Content-Type can force the correct value past a stubborn default; verify nothing overrides it afterwards.
3. .htaccess ignored due to AllowOverride
Anti-pattern: Dropping an .htaccess with AddType into a directory where AllowOverride None is set.
Effect: Apache never reads the file’s directives; the type stays wrong and there is no error to point you at the cause.
Fix: Set AllowOverride FileInfo (or All) on the <Directory>, or move the AddType into the vhost. Re-test after reload.
4. Caddy header vs mime confusion
Anti-pattern: Setting both a mime map and a header @avif Content-Type for the same path, expecting them to cooperate.
Effect: Two mechanisms compete to set Content-Type; the effective value depends on directive ordering and is hard to reason about — occasionally you get a duplicated or unexpected header.
Fix: Choose one idiom per path. Use header when you also need Vary/nosniff on that matcher; use mime for a clean extension→type map with no extra headers.
5. Setting Vary: Accept on video
Anti-pattern: Adding Vary: Accept to .webm/.mp4 responses alongside AVIF.
Effect: Video format selection happens via <source> order, not Accept negotiation, so Vary: Accept on video just fragments the CDN cache for no benefit.
Fix: Scope Vary: Accept to image extensions only, exactly as the parent MIME configuration guide recommends. Leave video responses without it.
Related
- MIME type configuration for modern media servers — the full Nginx, Apache, Caddy, and Cloudflare Worker registration reference and the
Vary: Acceptrules - Debugging incorrect Content-Type headers for WebM videos — the video-side counterpart to this AVIF fix
- How to configure AVIF fallbacks for Safari 14 — the
<picture>chain that relies on correct AVIF MIME registration - Cache-Control headers for image and video assets — pair correct MIME types with
immutableandVarycaching - Core Media Fundamentals & Next-Gen Formats — parent section covering the full media delivery pipeline