dotvitals

A compressed response does not vary on Accept-Encoding

LowConfirmedQuick winhttp.headers.vary-missing-accept-encoding

What this check looks for

The server sends different versions of this page depending on what the visitor's browser can unpack, but it does not tell shared caches that. A cache can hand the compressed copy to someone who cannot read it.

Why it matters

The symptom is a broken page for a fraction of visitors that nobody can reproduce, because it depends on which copy a shared cache happened to store first. It is one header, and every compression module can add it for you.

When the check passes, your report says: “The response varies on Accept-Encoding, so shared caches stay safe”.

What it costs your score

When this check fails it removes 5 points from your HTTP score, before the status, confidence and repeat multipliers are applied. HTTP carries a weight of 7 in the overall score.

It shares the http.compression family ceiling of 18 points: however many findings that family produces, together they cannot remove more than that from HTTP. One underlying problem showing up in several places is still one problem.

Severity
low
Default confidence
confirmed
Status when triggered
warn
Deduction
5 points
Family cap
http.compression · 18
Category
HTTP
Module
Http headers
Fix owned by
user
In the ruleset since
2026.09

How the whole score is calculated

How to fix it

Send Vary: Accept-Encoding on every response that may be compressed.

Without it a shared cache can serve a compressed body to a client that cannot decode it.

  1. nginx: set gzip_vary on; — it adds the header for you whenever gzip applies.

  2. Apache: mod_deflate adds it automatically for the types listed in AddOutputFilterByType; check nothing later overwrites Vary.

  3. Caddy: the encode directive sets it for you.

  4. If you add it by hand, append to any existing Vary rather than replacing it — losing a Vary: Cookie this way is far worse than the problem you are fixing.

  5. Never answer this with Vary: *; that disables shared caching entirely and is reported separately.

How to confirm it worked

  • curl -sSI -H 'Accept-Encoding: gzip, br' https://‹host›/ | grep -i '^vary:' — expect Accept-Encoding in the list

The configuration to publish
Vary: Accept-Encoding

A named slot like ‹domain› — and the braces left in the configuration below — is filled in with your own values when this rule appears on a report.

Remediation by platform

nginx
gzip on;
gzip_vary on;
gzip_min_length 256;
gzip_comp_level 5;
gzip_types text/plain text/css text/xml application/javascript application/json image/svg+xml;

# Brotli, if the ngx_brotli module is available:
# brotli on;
# brotli_types text/plain text/css text/xml application/javascript application/json image/svg+xml;
Apache
<IfModule mod_deflate.c>
	AddOutputFilterByType DEFLATE text/html text/plain text/css text/xml application/javascript application/json image/svg+xml
</IfModule>

<IfModule mod_brotli.c>
	AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json
</IfModule>
Caddy
encode zstd gzip
Cloudflare
Speed → Optimization → Content Optimization, or Rules → Overview → Create rule → Compression Rule to choose the algorithm per path. Dashboard paths as at 2026-09.
  • text/html is always compressed by nginx and must not be listed in gzip_types — listing it logs a duplicate-MIME warning. Brotli is not part of a stock nginx build: brotli on; fails to start a server without the ngx_brotli module compiled in, so leave those lines commented until nginx -V shows it.

  • Shown because the response identified Apache. Put it in the virtual host or server configuration rather than .htaccess: .htaccess is re-read on every request, is often disabled by AllowOverride None, and runs too late to influence a response the virtual host generates itself. mod_deflate adds Vary: Accept-Encoding on its own. mod_brotli ships with Apache 2.4.26 and later but is not enabled by default — a2enmod brotli, or the equivalent LoadModule line, is required, and the IfModule guard keeps the configuration valid until then.

  • Caddy's encode directive negotiates the encoding and sets Vary: Accept-Encoding itself. A stock Caddy build offers gzip and zstd; Brotli is not one of its on-the-fly encoders, so do not write encode br and expect it to work.

  • This applies only to responses that reach visitors through Cloudflare. An origin that is reachable directly — by IP, or through a DNS record that is not proxied — still serves the response measured here unchanged. Setting it at the origin as well covers both paths. Cloudflare compresses eligible responses by default, so a response measured as uncompressed here usually means the scan reached the origin directly.

Technical detail

The response from ‹origin› carried a Content-Encoding, so content negotiation took place, but Vary does not name Accept-Encoding. ‹detail›

RFC 9110 §12.5.5 defines Vary as the list of request header fields that were used to select this representation. A shared cache keys its stored copy on exactly those fields; omitting Accept-Encoding tells it that the compressed body is the only representation, so it will serve that body to a client that asked for identity.

A private browser cache rarely shows the fault, which is what makes this quietly dangerous: it appears only once a CDN, a corporate proxy or a shared reverse proxy is in the path. Every mainstream compression module can add the header itself — nginx's gzip_vary on;, Apache's mod_deflate, and Caddy's encode — so this is normally a matter of switching it on rather than writing it by hand. Detected server: ‹detected server›.

Standards and references

Test this on your domain

Run the check that produces this finding, on its own, against any domain.

Open the http headers checkerBuild the fix

Other http headers checks