# 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.

Source: https://howhttpworks.com/debug/err-content-decoding-failed
Last reviewed: 2026-10-05

Error messages this page covers:
- `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:`

> **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)](https://chromium.googlesource.com/chromium/src/+/main/net/base/net_error_list.h) 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`](https://howhttpworks.com/headers/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](https://www.rfc-editor.org/rfc/rfc9110.html#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:

```text
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](https://github.com/curl/curl/blob/curl-8_22_0/lib/content_encoding.c), the unsupported-coding message is shorter. Output from other versions may vary slightly:

```text
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](https://github.com/nginx/nginx/blob/master/src/http/modules/ngx_http_gunzip_filter_module.c) 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.

```bash
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`](https://curl.se/docs/manpage.html#--raw):

```bash
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](https://www.rfc-editor.org/rfc/rfc1952.html#section-2.3.1) 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:

```bash
# 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](https://developers.cloudflare.com/speed/optimization/content/compression/) 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](https://github.com/nginx/nginx/blob/master/src/http/modules/ngx_http_gzip_filter_module.c), 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:

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

PHP's [manual forbids combining `ob_gzhandler` with `zlib.output_compression`](https://www.php.net/manual/en/function.ob-gzhandler.php). If nginx will own compression, remove `ob_start('ob_gzhandler')` from the app and use this PHP configuration:

```ini
zlib.output_compression = Off
```

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

```nginx
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](https://nginx.org/en/docs/http/ngx_http_gzip_module.html#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](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header), 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

```bash
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](https://www.rfc-editor.org/rfc/rfc9110.html#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](https://howhttpworks.com/debug/err-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:

```bash
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](https://www.rfc-editor.org/rfc/rfc9111.html#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.

## Related

- [HTTP compression](https://howhttpworks.com/guides/http-compression)
- [Content-Encoding](https://howhttpworks.com/headers/content-encoding)
- [Vary](https://howhttpworks.com/headers/vary)
- [Incomplete chunked encoding](https://howhttpworks.com/debug/err-incomplete-chunked-encoding)
