How HTTP Works

Debug guide · you're seeing

  • ERR_CONTENT_DECODING_FAILED
  • net::ERR_CONTENT_DECODING_FAILED
  • curl: (61) Error while processing content unencoding: incorrect header check
  • curl: (61) Unrecognized content encoding type. libcurl understands deflate, gzip content encodings.
  • curl: (61) Unrecognized content encoding type
  • Z_DATA_ERROR: incorrect header check
  • Not a gzipped file (b'pl')
  • inflate() failed:

ERR_CONTENT_DECODING_FAILED: Fix Broken Compression

Fix ERR_CONTENT_DECODING_FAILED by comparing raw bytes with Content-Encoding, checking double compression, truncated gzip and cached encoding variants.

Reviewed 6 min readintermediate16 sourcesTry itMarkdown
On this page

TL;DR: The browser got a response it couldn’t decompress. Almost always the Content-Encoding header and the body disagree. Fetch the URL with curl without decoding and check the first bytes: if the header says gzip but the body doesn’t start with 1f 8b, fix whatever set that header. If it does start with 1f 8b, run gzip -t to find a truncated or corrupt stream.

What it means

Chromium defines ERR_CONTENT_DECODING_FAILED (-330) as a failure to decode the response body. It isn’t an HTTP status, and the server may have sent a perfectly normal 200 OK before the decoder choked on the body.

Content-Encoding names the compression applied to the body, such as gzip or Brotli. That’s a separate layer from HTTP/1.1 chunk framing. RFC 9110 Section 8.4 requires the header to list applied codings in application order; decoding reverses that order.

We captured these errors from deliberately broken local servers with curl 8.7.1 on macOS, using --compressed. The first response labelled plain\n as gzip. The second sent Content-Encoding: br to a curl build without Brotli support:

curl: (61) Error while processing content unencoding: incorrect header check
curl: (61) Unrecognized content encoding type. libcurl understands deflate, gzip content encodings.

The list of supported encodings depends on how curl was built. In curl 8.22.0’s source, the unsupported-coding message is shorter. Output from other versions may vary slightly:

curl: (61) Unrecognized content encoding type

Decoding the same plain\n bytes directly produced Z_DATA_ERROR: incorrect header check with Node 26.10.0 gunzipSync, and Not a gzipped file (b'pl') with Python 3.14.8 gzip.decompress. Those come from the decoders themselves; Node and Python HTTP clients may wrap them in their own messages. If nginx itself is decompressing an upstream response, its gunzip filter logs the prefix inflate() failed: followed by numeric flush and error codes.

Inspect the headers and bytes together

Use the exact URL that fails, with the same method, cookies, and authentication. Without them you might be testing a login redirect, which may compress differently from the API response you care about.

curl -sS -D gzip.headers -H 'Accept-Encoding: gzip' \
  -o body.gz 'https://example.com/failing-path'
xxd -l 16 body.gz
gzip -t body.gz
curl -sS --compressed -D decoded.headers \
  -o decoded.body 'https://example.com/failing-path'

The first request advertises gzip but does not turn on curl’s automatic decoding, so body.gz holds the bytes as sent (curl still strips HTTP/1.1 chunk framing). Run gzip -t only if the response actually declared gzip. Check the exit status and stderr of each command.

For a quick look at the body as it came over the wire, add --raw:

curl -s -H 'Accept-Encoding: gzip' --raw \
  'https://example.com/failing-path' | xxd | head

--raw keeps the transfer coding too. A chunked HTTP/1.1 response starts with an ASCII chunk size and a CRLF before the compressed bytes, so 1f 8b may not be at offset zero. Validate against the saved, dechunked body.gz instead. RFC 1952 defines those two gzip magic bytes. This preview only shows the start of the body; head can cut the download short, so it can’t tell you whether the stream is complete.

Fix it, in diagnostic order

1. The header says gzip but the body is plain text

Look at gzip.headers and the xxd -l 16 body.gz output from the capture above. Plain HTML or JSON under a gzip header is your mismatch. Common causes: the app sets Content-Encoding by hand, a proxy decompresses the body but leaves the header, or an error handler swaps in a new body after the compression headers were set.

Fix it where the header is generated: remove the false header, or actually compress the body. Stripping Content-Encoding while the bytes are still compressed just creates the opposite mismatch.

2. Two layers disagree about compression

Compare origin and public responses with the same gzip-only request:

# Use the real origin IP; --resolve preserves the hostname and TLS SNI.
curl -sS --resolve example.com:443:192.0.2.10 \
  -H 'Accept-Encoding: gzip' -D origin.headers \
  -o origin.gz 'https://example.com/failing-path'
gzip -t origin.gz
gzip -dc body.gz > once-decoded.body
xxd -l 16 once-decoded.body

If decompressing once leaves another gzip header, the body is gzipped twice. That’s legal if the response declares both layers as Content-Encoding: gzip, gzip. With only one layer declared, the browser decodes once and hands still-compressed bytes to the HTML or JSON parser. You’ll often see garbage or a parse error then, not necessarily Chrome’s decoding error.

Check the app, proxy, and CDN one at a time. Cloudflare decompresses and recompresses for some transformations, which is different from stacking a second layer. And nginx’s gzip filter skips responses that already have a nonempty Content-Encoding, so having two compressors switched on doesn’t mean both are running on the same response.

For PHP, find any output handlers and check the configuration the web SAPI actually loads:

grep -rnE 'ob_gzhandler|Content-Encoding|gzencode' /path/to/app

PHP’s manual forbids combining ob_gzhandler with zlib.output_compression. If nginx will own compression, remove ob_start('ob_gzhandler') from the app and use this PHP configuration:

zlib.output_compression = Off

An nginx configuration for an HTTP upstream that should send uncompressed responses:

location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Accept-Encoding identity;
    gzip on;
    gzip_types application/json text/css application/javascript;
    gzip_vary on;
}

nginx already includes text/html in its gzip types. Sending identity upstream asks the app for unencoded output, and the app has to honor it. Use identity rather than proxy_set_header Accept-Encoding "": nginx drops fields whose configured value is empty, and a missing Accept-Encoding means any coding is acceptable. This proxy_pass example doesn’t cover PHP-FPM, so check that pool’s settings separately. Run nginx -t before reloading.

3. Brotli or zstd was selected outside the client’s accepted set

curl --version
curl -v --compressed -H 'Accept-Encoding: gzip' \
  -o /dev/null 'https://example.com/failing-path'
curl -v -H 'Accept-Encoding: identity' \
  -o /dev/null 'https://example.com/failing-path'

Compare the outgoing Accept-Encoding with the incoming Content-Encoding. If a gzip-only request gets br or zstd back, something in negotiation or cache selection is wrong, and a client without that decoder has no way to read the body. Fix the selection at whichever layer made it, rather than hard-coding a different response header.

Make sure the request actually sends Accept-Encoding when you test this. An absent header proves nothing here: RFC 9110 Section 12.5.3 treats absence as accepting any coding. An explicitly empty field requests no coding. identity requests an unencoded representation.

4. The compressed stream was truncated

Run gzip -t body.gz on the full capture. A stream can start with a perfect 1f 8b and still be missing its end or contain invalid compressed data. Compare with gzip -t origin.gz and any curl transfer errors, then check app exceptions and proxy logs around the time of the failure. The fix belongs in whatever cut the stream off, whether that’s the producer or the connection; the decompressor is doing its job. For missing HTTP/1.1 termination, follow incomplete chunked encoding.

5. A cache mixed encoding variants

Fetch the same URL with alternating encodings. Keep the URL identical, with no cache-busting query, so both requests hit the same cache entry:

curl -sS -H 'Accept-Encoding: gzip' -D cached-gzip.headers \
  -o cached.gz 'https://example.com/failing-path'
curl -sS -H 'Accept-Encoding: identity' -D cached-identity.headers \
  -o cached-identity.body 'https://example.com/failing-path'

Compare the bodies, Content-Encoding, Vary, and any cache diagnostics your provider documents. RFC 9111 Section 4.1 uses Vary to select cached responses by request fields. Send Vary: Accept-Encoding on any response whose encoding depends on the request, keeping any other Vary fields; gzip_vary on does this for nginx’s gzip filter. Fix the cache’s variant handling and purge the affected entries. Vary won’t repair a body that’s already corrupt. And if the reused gzip is valid and the client supports it, a missing Vary isn’t the cause of your decoding error.

Frequently asked questions

What does ERR_CONTENT_DECODING_FAILED mean?

Chrome received the response but could not decompress the body. Usually the Content-Encoding header says gzip or br while the bytes are something else, or the compressed stream was cut short. The status can still be 200, so compare the header with the actual bytes, then check that the stream is complete.

Does enabling gzip in both PHP and nginx always double-compress the response?

No. The nginx gzip filter skips any response that already has a nonempty Content-Encoding. If you see double compression, look for a layer that strips the header or for custom compression middleware.

Why does curl work without --compressed but fail with it?

Without --compressed, curl saves the encoded bytes as they arrive and never tries to decompress them. With it, curl asks for the encodings it supports and decodes the body, which is when an unsupported or broken encoding shows up.

Can missing Vary: Accept-Encoding cause this error?

Indirectly. Without Vary, a cache can hand a gzip or Brotli variant to a client that asked for something else. Decoding fails when that body is unsupported by the client, corrupt, or labelled wrongly. A valid gzip body served to a gzip-capable client still decodes fine.

Sources

  1. MDN: Content-Encodingdeveloper.mozilla.org
  2. RFC 9110: Content-Encoding and Accept-Encodingrfc-editor.org
  3. RFC 9111: cache keys and Varyrfc-editor.org
  4. RFC 1952: gzip magic bytesrfc-editor.org
  5. Chromium source: CONTENT_DECODING_FAILEDchromium.googlesource.com
  6. curl 8.7.1 source: decoding errorsgithub.com
  7. curl 8.22.0 source: decoding errorsgithub.com
  8. curl: --raw and --compressedcurl.se
  9. nginx: gzip and gzip_varynginx.org
  10. nginx source: skip already encoded responsesgithub.com
  11. nginx source: gunzip error logginggithub.com
  12. PHP: ob_gzhandlerphp.net
  13. PHP: zlib.output_compressionphp.net
  14. Cloudflare: content compression and recompressiondevelopers.cloudflare.com
  15. Node.js: zlib decompressionnodejs.org
  16. Python: gzip exceptionsdocs.python.org

Keep going

Browse /search