# Accept-Encoding defaults: lab matrix

**Synthetic loopback results.** A 127.0.0.1 origin and a counting proxy served local files. No public site was measured, and no byte count here is a provider bill.

Runs 2026-09-27T02:44:37Z to 2026-09-27T02:45:53Z (UTC), 567 records (567 run). Harness tree SHA-256 `c586261f08f49693`.

Byte counts use the synthetic fixture, 100,129 B of HTML. Its ratios are a fixture ratio, not typical.

Wire body bytes exclude response headers, TLS records, the CONNECT exchange and retries, so they are not what a provider bills.

## Who asks for compression by default

### Sends no Accept-Encoding, or identity

A server that compresses only when asked sends these clients uncompressed text. No header: curl, node:http, node:https, PHP ext-curl, Guzzle, Laravel Http, Java HttpClient. `identity`: Wget, urllib, http.client.

| Client | Fix |
|---|---|
| curl | Add `--compressed` |
| Wget | Add `--compression=auto` |
| urllib, http.client | Use Requests or httpx |
| node:http, node:https | Use `fetch`, got or axios |
| PHP ext-curl | Set `CURLOPT_ACCEPT_ENCODING` to `''` |
| Guzzle, Laravel Http | Set `decode_content` to `'gzip'` |
| Java HttpClient | Send `Accept-Encoding: gzip`, unzip with `GZIPInputStream` |

Versions run: curl 8.7.1, 8.22.0; Wget 1.25.0; urllib.request, http.client (Python 3.13.15, 3.14.7); node:http, node:https (Node.js 22.23.3, 24.21.0, 26.10.0); PHP 8.5.8 ext-curl (libcurl 8.20.0); Guzzle 8.2.0, Laravel Http 13.33.0; Java 25.0.4.1 HttpClient.

- curl: a hand-set `-H 'Accept-Encoding: gzip'` is sent, but the body stays compressed.
- Wget: with `--compression=auto` it sent `gzip` and decoded the reply.
- urllib.request and http.client return compressed bodies as they arrived.
- node:http and node:https return compressed bodies as they arrived.
- PHP ext-curl: with `CURLOPT_ACCEPT_ENCODING` set to `''`, this libcurl sent `deflate, gzip, br, zstd`. A hand-set header is sent but not decoded.
- Laravel Http: `withOptions(['decode_content' => 'gzip'])`.
- Java HttpClient never decodes: wrap the body in `GZIPInputStream` when the reply is gzip.

### Already asks for compression

Nothing to change: these clients ask for compression and decode the reply.

| Client | Header sent |
|---|---|
| Requests | Python 3.13.15: `gzip, deflate`; Python 3.14.7: `gzip, deflate, zstd` |
| httpx | `gzip, deflate` |
| aiohttp | Python 3.13.15: `gzip, deflate`; Python 3.14.7: `gzip, deflate, zstd` |
| Scrapy | `gzip, deflate, br, zstd` |
| Node.js built-in fetch | Node.js 22.23.3/24.21.0: `br, gzip, deflate`; Node.js 26.10.0: `br, gzip, deflate, zstd` |
| Go net/http | `gzip` |

Versions run: Requests 2.34.2; httpx 0.28.1; aiohttp 3.14.3; Scrapy 2.19.0; built-in fetch (Node.js 22.23.3, 24.21.0, 26.10.0); Go 1.27.1 net/http.

- Requests: `brotli` adds br; on Python 3.13, `backports.zstd` adds zstd.
- httpx: `brotli` adds br and `zstandard` adds zstd.
- aiohttp: `Brotli` adds br; on Python 3.13, `backports.zstd` adds zstd.
- Built-in fetch over `http://`: `gzip, deflate`.
- Go: setting the header yourself turns decoding off.

### Where each row comes from

| Row | Basis | Records | Cell definitions |
|---|---|---|---|
| curl 8.7.1, 8.22.0 | run | 22 | `harness/cells/core.json`, `harness/cells/pitfalls.json` |
| Wget 1.25.0 | run | 7 | `harness/cells/core.json`, `harness/cells/pitfalls.json` |
| urllib.request, http.client (Python 3.13.15, 3.14.7) | run | 36 | `harness/cells/core.json`, `harness/cells/pitfalls.json` |
| node:http, node:https (Node.js 22.23.3, 24.21.0, 26.10.0) | run | 57 | `harness/cells/node-go/node.json` |
| PHP 8.5.8 ext-curl (libcurl 8.20.0) | run | 10 | `harness/cells/php-java/php-java-core.json`, `harness/cells/php-java/php-java-pitfalls.json` |
| Guzzle 8.2.0, Laravel Http 13.33.0 | run | 18 | `harness/cells/php-java/php-java-core.json` |
| Java 25.0.4.1 HttpClient | run | 8 | `harness/cells/php-java/php-java-core.json`, `harness/cells/php-java/php-java-pitfalls.json` |
| Requests 2.34.2 | run | 15 | `harness/cells/core.json` |
| httpx 0.28.1 | run | 18 | `harness/cells/core.json` |
| aiohttp 3.14.3 | run | 15 | `harness/cells/core.json` |
| Scrapy 2.19.0 | run | 14 | `harness/cells/core.json`, `harness/cells/pitfalls.json` |
| built-in fetch (Node.js 22.23.3, 24.21.0, 26.10.0) | run | 12 | `harness/cells/node-go/node.json` |
| Go 1.27.1 net/http | run | 12 | `harness/cells/node-go/go.json` |

## Per-client detail

One row per setup. Paths that behaved the same are merged into one row.

### curl

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| curl 8.7.1 (macOS) · default | direct http://, proxy http://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |
| curl 8.7.1 (macOS) · `--compressed` | direct http://, proxy http://, proxy CONNECT | `deflate, gzip` | `gzip`, 17,750 B | decoded, 100,129 B |
| curl 8.7.1 (macOS) · `-H 'Accept-Encoding: gzip'` | direct http://, proxy http://, proxy CONNECT | `gzip` | `gzip`, 17,750 B | still `gzip`, 17,750 B |
| curl 8.7.1 (macOS) · `--compressed --no-compressed` | direct http:// | (none) | not compressed, 100,129 B | 100,129 B |
| curl 8.22.0 (Homebrew) · default | direct http://, proxy http://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |
| curl 8.22.0 (Homebrew) · `--compressed` | direct http://, proxy http://, proxy CONNECT | `deflate, gzip, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| curl 8.22.0 (Homebrew) · `-H 'Accept-Encoding: gzip'` | direct http://, proxy http://, proxy CONNECT | `gzip` | `gzip`, 17,750 B | still `gzip`, 17,750 B |
| curl 8.22.0 (Homebrew) · `--compressed --no-compressed` | direct http:// | (none) | not compressed, 100,129 B | 100,129 B |

### Wget

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| Wget 1.25.0 · default | direct http://, proxy http://, proxy CONNECT | `identity` | not compressed, 100,129 B | 100,129 B |
| Wget 1.25.0 · `--compression=auto` | direct http://, proxy http://, proxy CONNECT | `gzip` | `gzip`, 17,750 B | decoded, 100,129 B |

### Python standard library

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| urllib.request, Python 3.13.15 · standard library | direct http://, proxy http://, proxy CONNECT | `identity` | not compressed, 100,129 B | 100,129 B |
| http.client, Python 3.13.15 · standard library | direct http://, proxy http://, proxy CONNECT | `identity` | not compressed, 100,129 B | 100,129 B |
| urllib.request, Python 3.14.7 · standard library | direct http://, proxy http://, proxy CONNECT | `identity` | not compressed, 100,129 B | 100,129 B |
| http.client, Python 3.14.7 · standard library | direct http://, proxy http://, proxy CONNECT | `identity` | not compressed, 100,129 B | 100,129 B |

### Requests

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| Requests 2.34.2, Python 3.13.15 · bare install | direct http://, proxy http://, proxy CONNECT | `gzip, deflate` | `gzip`, 17,750 B | decoded, 100,129 B |
| Requests 2.34.2, Python 3.13.15 · + `brotli` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, br` | `br`, 18,451 B | decoded, 100,129 B |
| Requests 2.34.2, Python 3.13.15 · + `backports.zstd` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| Requests 2.34.2, Python 3.14.7 · bare install | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| Requests 2.34.2, Python 3.14.7 · + `brotli` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |

### httpx

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| httpx 0.28.1, Python 3.13.15 · bare install | direct http://, proxy http://, proxy CONNECT | `gzip, deflate` | `gzip`, 17,750 B | decoded, 100,129 B |
| httpx 0.28.1, Python 3.13.15 · + `brotli` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, br` | `br`, 18,451 B | decoded, 100,129 B |
| httpx 0.28.1, Python 3.13.15 · + `zstandard` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| httpx 0.28.1, Python 3.14.7 · bare install | direct http://, proxy http://, proxy CONNECT | `gzip, deflate` | `gzip`, 17,750 B | decoded, 100,129 B |
| httpx 0.28.1, Python 3.14.7 · + `brotli` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, br` | `br`, 18,451 B | decoded, 100,129 B |
| httpx 0.28.1, Python 3.14.7 · + `zstandard` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |

### aiohttp

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| aiohttp 3.14.3, Python 3.13.15 · bare install | direct http://, proxy http://, proxy CONNECT | `gzip, deflate` | `gzip`, 17,750 B | decoded, 100,129 B |
| aiohttp 3.14.3, Python 3.13.15 · + `Brotli` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, br` | `br`, 18,451 B | decoded, 100,129 B |
| aiohttp 3.14.3, Python 3.13.15 · + `backports.zstd` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| aiohttp 3.14.3, Python 3.14.7 · bare install | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| aiohttp 3.14.3, Python 3.14.7 · + `Brotli` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |

### Scrapy

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| Scrapy 2.19.0, Python 3.13.15 · default dependencies | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| Scrapy 2.19.0, Python 3.14.7 · default dependencies | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |

### Node.js built-in fetch

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| fetch, Node.js 22.23.3 · default | direct http://, proxy http:// (sent as CONNECT) | `gzip, deflate` | `gzip`, 17,750 B | decoded, 100,129 B |
| fetch, Node.js 22.23.3 · default | direct https://, proxy CONNECT | `br, gzip, deflate` | `br`, 18,451 B | decoded, 100,129 B |
| fetch, Node.js 24.21.0 · default | direct http://, proxy http:// (sent as CONNECT) | `gzip, deflate` | `gzip`, 17,750 B | decoded, 100,129 B |
| fetch, Node.js 24.21.0 · default | direct https://, proxy CONNECT | `br, gzip, deflate` | `br`, 18,451 B | decoded, 100,129 B |
| fetch, Node.js 26.10.0 · default | direct http://, proxy http:// | `gzip, deflate` | `gzip`, 17,750 B | decoded, 100,129 B |
| fetch, Node.js 26.10.0 · default | direct https://, proxy CONNECT | `br, gzip, deflate, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |

### Node.js node:http and node:https

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| node:http, Node.js 22.23.3 · default | direct http://, proxy http:// | (none) | not compressed, 100,129 B | 100,129 B |
| node:https, Node.js 22.23.3 · default | direct https://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |
| node:http, Node.js 24.21.0 · default | direct http://, proxy http:// | (none) | not compressed, 100,129 B | 100,129 B |
| node:https, Node.js 24.21.0 · default | direct https://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |
| node:http, Node.js 26.10.0 · default | direct http://, proxy http:// | (none) | not compressed, 100,129 B | 100,129 B |
| node:https, Node.js 26.10.0 · default | direct https://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |

### Node.js libraries

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| axios 1.20.0, Node.js 22.23.3 · default | direct http://, direct https://, proxy http://, proxy CONNECT | `gzip, compress, deflate, br` | `br`, 18,451 B | decoded, 100,129 B |
| got 16.0.0, Node.js 22.23.3 · default | direct http://, direct https://, proxy http://, proxy CONNECT | `gzip, deflate, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| axios 1.20.0, Node.js 24.21.0 · default | direct http://, direct https://, proxy http://, proxy CONNECT | `gzip, compress, deflate, br` | `br`, 18,451 B | decoded, 100,129 B |
| got 16.0.0, Node.js 24.21.0 · default | direct http://, direct https://, proxy http://, proxy CONNECT | `gzip, deflate, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| axios 1.20.0, Node.js 26.10.0 · default | direct http://, direct https://, proxy http://, proxy CONNECT | `gzip, compress, deflate, br` | `br`, 18,451 B | decoded, 100,129 B |
| got 16.0.0, Node.js 26.10.0 · default | direct http://, direct https://, proxy http://, proxy CONNECT | `gzip, deflate, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| node-fetch 3.3.2, Node.js 24.21.0 · default | direct http://, direct https:// | `gzip, deflate, br` | `br`, 18,451 B | decoded, 100,129 B |
| node-fetch 2.7.0, Node.js 24.21.0 · default | direct http://, direct https:// | `gzip,deflate` | `gzip`, 17,750 B | decoded, 100,129 B |
| axios 1.20.0, Node.js 24.21.0 · `advertiseZstdAcceptEncoding: true` | direct https:// | `gzip, compress, deflate, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |

### Go net/http

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| Go 1.27.1 · default | direct http://, direct https://, proxy http://, proxy CONNECT | `gzip` | `gzip`, 17,750 B | decoded, 100,129 B |
| Go 1.27.1 · `DisableCompression: true` | direct http://, direct https://, proxy http://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |
| Go 1.27.1 · `req.Header.Set("Accept-Encoding", "gzip")` | direct http://, direct https://, proxy http://, proxy CONNECT | `gzip` | `gzip`, 17,750 B | still `gzip`, 17,750 B |

### PHP: ext-curl, Guzzle and Laravel Http

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| PHP 8.5.8 ext-curl (libcurl 8.20.0) · default | direct http://, proxy http://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |
| PHP 8.5.8 ext-curl (libcurl 8.20.0) · `CURLOPT_ACCEPT_ENCODING => ''` | direct http://, proxy http://, proxy CONNECT | `deflate, gzip, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| PHP 8.5.8 ext-curl (libcurl 8.20.0) · `CURLOPT_HTTPHEADER`, `Accept-Encoding: gzip` | direct http://, proxy http://, proxy CONNECT | `gzip` | `gzip`, 17,750 B | still `gzip`, 17,750 B |
| Guzzle 8.2.0 · default | direct http://, proxy http://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |
| Guzzle 8.2.0 · `'decode_content' => 'gzip'` | direct http://, proxy http://, proxy CONNECT | `gzip` | `gzip`, 17,750 B | decoded, 100,129 B |
| Guzzle 8.2.0 · `'decode_content' => 'gzip, deflate, br, zstd'` | direct http://, proxy http://, proxy CONNECT | `gzip, deflate, br, zstd` | `zstd`, 19,298 B | decoded, 100,129 B |
| Guzzle 8.2.0 StreamHandler · default | direct http://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |
| Guzzle 8.2.0 StreamHandler · `'decode_content' => 'gzip'` | direct http://, proxy CONNECT | `gzip` | `gzip`, 17,750 B | decoded, 100,129 B |
| Guzzle 8.2.0 StreamHandler · default + `request_fulluri` | proxy http:// | (none) | not compressed, 100,129 B | 100,129 B |
| Laravel Http 13.33.0 · default | direct http://, proxy http://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |
| Laravel Http 13.33.0 · `withHeaders(['Accept-Encoding' => 'gzip'])` | direct http://, proxy http://, proxy CONNECT | `gzip` | `gzip`, 17,750 B | decoded, 100,129 B |
| Laravel Http 13.33.0 · `withOptions(['decode_content' => 'gzip'])` | direct http:// | `gzip` | `gzip`, 17,750 B | decoded, 100,129 B |

### Java java.net.http.HttpClient

| Setup | Paths | Sent | Server sent (fixture) | Your code got |
|---|---|---|---|---|
| Java 25.0.4.1 HttpClient · default | direct http://, proxy http://, proxy CONNECT | (none) | not compressed, 100,129 B | 100,129 B |
| Java 25.0.4.1 HttpClient · `Accept-Encoding: gzip` + `GZIPInputStream` | direct http://, proxy http://, proxy CONNECT | `gzip` | `gzip`, 17,750 B | decoded, 100,129 B |

## Bytes with and without compression

### Fixture over CONNECT, same client

Fixture ratio, not typical. The proxy leg adds response headers, TLS records and the CONNECT reply to the body, so its ratio is lower.

| Client | Body bytes: without → with | Proxy → client bytes: without → with | Ratio: body / proxy leg |
|---|---|---|---|
| curl 8.7.1 (macOS): default → `--compressed` (`gzip`) | 100,129 → 17,750 | 102,559 → 20,093 | 5.64× / 5.10× |
| curl 8.22.0 (Homebrew): default → `--compressed` (`zstd`) | 100,129 → 19,298 | 103,625 → 22,707 | 5.19× / 4.56× |
| Wget 1.25.0: default → `--compression=auto` (`gzip`) | 100,129 → 17,750 | 103,602 → 21,136 | 5.64× / 4.90× |
| PHP 8.5.8 ext-curl (libcurl 8.20.0): default → `CURLOPT_ACCEPT_ENCODING => ''` (`zstd`) | 100,129 → 19,298 | 103,625 → 22,707 | 5.19× / 4.56× |
| Guzzle 8.2.0: default → `'decode_content' => 'gzip'` (`gzip`) | 100,129 → 17,750 | 103,625 → 21,159 | 5.64× / 4.90× |
| Laravel Http 13.33.0: default → `withHeaders(['Accept-Encoding' => 'gzip'])` (`gzip`) | 100,129 → 17,750 | 103,625 → 21,159 | 5.64× / 4.90× |
| Java 25.0.4.1 HttpClient: default → `Accept-Encoding: gzip` + `GZIPInputStream` (`gzip`) | 100,129 → 17,750 | 102,583 → 20,117 | 5.64× / 5.10× |
| Go 1.27.1: `DisableCompression: true` → default (`gzip`) | 100,129 → 17,750 | 103,649 → 21,183 | 5.64× / 4.89× |

### Corpus: 23 CPython 3.14.7 documentation pages

This corpus only, not typical of the web. Each row is one client fetching all 23 pages through CONNECT.

| Client | Coding | Body bytes: identity → wire | Ratio |
|---|---|---|---|
| curl 8.22.0 (Homebrew) · default | not compressed | 2,175,071 → 2,175,071 | 1.00× |
| curl 8.7.1 (macOS) · `--compressed` | `gzip` | 2,175,071 → 325,873 | 6.67× |
| curl 8.22.0 (Homebrew) · `--compressed` | `zstd` | 2,175,071 → 329,111 | 6.61× |
| Go 1.27.1 · default | `gzip` | 2,175,071 → 325,873 | 6.67× |
| fetch, Node.js 24.21.0 · default | `br` | 2,175,071 → 277,665 | 7.83× |
| fetch, Node.js 26.10.0 · default | `zstd` | 2,175,071 → 329,111 | 6.61× |

## What Node.js fetch reports about body bytes

After reading the body, the client looked up the `performance.getEntriesByType('resource')` entry for its URL. Proxy → client bytes are the counting proxy's own count for the same request.

| Node.js | Path | Server sent | Wire body bytes | `encodedBodySize` | `decodedBodySize` | `transferSize` | Proxy → client bytes |
|---|---|---|---|---|---|---|---|
| 22.23.3 | direct http:// | `gzip`, 17,750 B | 17,750 | 17,750 | 100,129 | 18,050 | — |
| 22.23.3 | direct http:// | not compressed, 100,129 B | 100,129 | 100,129 | 100,129 | 100,429 | — |
| 22.23.3 | direct https:// | `br`, 18,451 B | 18,451 | 18,451 | 100,129 | 18,751 | — |
| 22.23.3 | direct https:// | not compressed, 100,129 B | 100,129 | 100,129 | 100,129 | 100,429 | — |
| 22.23.3 | proxy CONNECT | `br`, 18,451 B | 18,451 | 18,451 | 100,129 | 18,751 | 21,882 |
| 22.23.3 | proxy CONNECT | not compressed, 100,129 B | 100,129 | 100,129 | 100,129 | 100,429 | 103,649 |
| 24.21.0 | direct http:// | `gzip`, 17,750 B | 17,750 | 17,750 | 100,129 | 18,050 | — |
| 24.21.0 | direct http:// | not compressed, 100,129 B | 100,129 | 100,129 | 100,129 | 100,429 | — |
| 24.21.0 | direct https:// | `br`, 18,451 B | 18,451 | 18,451 | 100,129 | 18,751 | — |
| 24.21.0 | direct https:// | not compressed, 100,129 B | 100,129 | 100,129 | 100,129 | 100,429 | — |
| 24.21.0 | proxy CONNECT | `br`, 18,451 B | 18,451 | 18,451 | 100,129 | 18,751 | 21,882 |
| 24.21.0 | proxy CONNECT | not compressed, 100,129 B | 100,129 | 100,129 | 100,129 | 100,429 | 103,649 |
| 26.10.0 | direct http:// | `gzip`, 17,750 B | 17,750 | 17,750 | 100,129 | 18,050 | — |
| 26.10.0 | direct http:// | not compressed, 100,129 B | 100,129 | 100,129 | 100,129 | 100,429 | — |
| 26.10.0 | direct https:// | `zstd`, 19,298 B | 19,298 | 19,298 | 100,129 | 19,598 | — |
| 26.10.0 | direct https:// | not compressed, 100,129 B | 100,129 | 100,129 | 100,129 | 100,429 | — |
| 26.10.0 | proxy CONNECT | `zstd`, 19,298 B | 19,298 | 19,298 | 100,129 | 19,598 | 22,731 |
| 26.10.0 | proxy CONNECT | not compressed, 100,129 B | 100,129 | 100,129 | 100,129 | 100,429 | 103,649 |

In all 18 rows, `encodedBodySize` equals the body bytes the origin wrote and `decodedBodySize` equals the bytes your code received.

`transferSize` was `encodedBodySize` + 300 B in every row, whether the request went direct or through the proxy: a fixed allowance in Node.js, not a count of header, TLS or CONNECT bytes.

In every row the entry was not yet visible synchronously right after the body was read; it appeared after 1 event-loop turn(s) (`await new Promise(setImmediate)`).

## Pitfalls

### The server compresses without being asked

The origin sent gzip even though the client asked for none.

| Client | Sent | Your code got |
|---|---|---|
| curl 8.7.1 (macOS) · default | (none) | still `gzip`, 17,750 B |
| curl 8.22.0 (Homebrew) · default | (none) | still `gzip`, 17,750 B |
| Wget 1.25.0 · default | `identity` | still `gzip`, 17,750 B |
| urllib.request, Python 3.13.15 · standard library | `identity` | still `gzip`, 17,750 B |
| urllib.request, Python 3.14.7 · standard library | `identity` | still `gzip`, 17,750 B |
| Go 1.27.1 · `DisableCompression: true` | (none) | still `gzip`, 17,750 B |
| node:http, Node.js 22.23.3 · default | (none) | still `gzip`, 17,750 B |
| node:http, Node.js 24.21.0 · default | (none) | still `gzip`, 17,750 B |
| node:http, Node.js 26.10.0 · default | (none) | still `gzip`, 17,750 B |
| PHP 8.5.8 ext-curl (libcurl 8.20.0) · default | (none) | still `gzip`, 17,750 B |
| Guzzle 8.2.0 · default | (none) | decoded, 100,129 B |
| Guzzle 8.2.0 StreamHandler · default | (none) | decoded, 100,129 B |
| Laravel Http 13.33.0 · default | (none) | decoded, 100,129 B |
| Java 25.0.4.1 HttpClient · default | (none) | still `gzip`, 17,750 B |

### Which codings each client decodes

The origin forced each coding regardless of the request. "zstd ×4" is four concatenated zstd frames, which RFC 8878 allows.

| Client | gzip | br | zstd | zstd ×4 |
|---|---|---|---|---|
| curl 8.7.1 (macOS) · `--compressed` | decoded | error | error | error |
| curl 8.22.0 (Homebrew) · `--compressed` | decoded | decoded | decoded | decoded |
| Wget 1.25.0 · `--compression=auto` | decoded | raw bytes | raw bytes | raw bytes |
| urllib.request, Python 3.13.15 · standard library | raw bytes | raw bytes | raw bytes | raw bytes |
| Requests 2.34.2, Python 3.13.15 · bare install | decoded | raw bytes | raw bytes | raw bytes |
| Requests 2.34.2, Python 3.13.15 · + `brotli` | decoded | decoded | raw bytes | raw bytes |
| Requests 2.34.2, Python 3.13.15 · + `backports.zstd` | decoded | raw bytes | decoded | decoded |
| httpx 0.28.1, Python 3.13.15 · bare install | decoded | raw bytes | raw bytes | raw bytes |
| httpx 0.28.1, Python 3.13.15 · + `brotli` | decoded | decoded | raw bytes | raw bytes |
| httpx 0.28.1, Python 3.13.15 · + `zstandard` | decoded | raw bytes | decoded | decoded |
| aiohttp 3.14.3, Python 3.13.15 · bare install | decoded | error | error | error |
| aiohttp 3.14.3, Python 3.13.15 · + `Brotli` | decoded | decoded | error | error |
| aiohttp 3.14.3, Python 3.13.15 · + `backports.zstd` | decoded | error | decoded | decoded |
| Scrapy 2.19.0, Python 3.13.15 · default dependencies | decoded | decoded | decoded | decoded |
| urllib.request, Python 3.14.7 · standard library | raw bytes | raw bytes | raw bytes | raw bytes |
| Requests 2.34.2, Python 3.14.7 · bare install | decoded | raw bytes | decoded | decoded |
| Requests 2.34.2, Python 3.14.7 · + `brotli` | decoded | decoded | decoded | decoded |
| httpx 0.28.1, Python 3.14.7 · bare install | decoded | raw bytes | raw bytes | raw bytes |
| httpx 0.28.1, Python 3.14.7 · + `brotli` | decoded | decoded | raw bytes | raw bytes |
| httpx 0.28.1, Python 3.14.7 · + `zstandard` | decoded | raw bytes | decoded | decoded |
| aiohttp 3.14.3, Python 3.14.7 · bare install | decoded | error | decoded | decoded |
| aiohttp 3.14.3, Python 3.14.7 · + `Brotli` | decoded | decoded | decoded | decoded |
| Scrapy 2.19.0, Python 3.14.7 · default dependencies | decoded | decoded | decoded | decoded |
| Go 1.27.1 · default | decoded | raw bytes | raw bytes | — |
| fetch, Node.js 22.23.3 · default (http://) | decoded | decoded | raw bytes | raw bytes |
| axios 1.20.0, Node.js 22.23.3 · default | — | decoded | decoded | first frame only |
| got 16.0.0, Node.js 22.23.3 · default | — | decoded | decoded | first frame only |
| fetch, Node.js 24.21.0 · default (http://) | decoded | decoded | decoded | first frame only |
| axios 1.20.0, Node.js 24.21.0 · default | — | decoded | decoded | first frame only |
| got 16.0.0, Node.js 24.21.0 · default | — | decoded | decoded | first frame only |
| fetch, Node.js 26.10.0 · default (http://) | decoded | decoded | decoded | decoded |
| axios 1.20.0, Node.js 26.10.0 · default | — | decoded | decoded | decoded |
| got 16.0.0, Node.js 26.10.0 · default | — | decoded | decoded | decoded |
| PHP 8.5.8 ext-curl (libcurl 8.20.0) · `CURLOPT_ACCEPT_ENCODING => ''` | decoded | decoded | decoded | decoded |
| Guzzle 8.2.0 · default | decoded | decoded | decoded | decoded |
| Guzzle 8.2.0 StreamHandler · default | decoded | raw bytes | raw bytes | — |

### A copied browser header

The client was given `gzip, deflate, br, zstd` by hand.

| Client | Server chose zstd | Server forced br |
|---|---|---|
| curl 8.7.1 (macOS) · copied header | raw bytes | raw bytes |
| curl 8.22.0 (Homebrew) · copied header | raw bytes | raw bytes |
| urllib.request, Python 3.13.15 · standard library | raw bytes | raw bytes |
| urllib.request, Python 3.14.7 · standard library | raw bytes | raw bytes |
| Requests 2.34.2, Python 3.13.15 · bare install | raw bytes | raw bytes |
| Requests 2.34.2, Python 3.13.15 · + `brotli` | raw bytes | decoded |
| Requests 2.34.2, Python 3.14.7 · bare install | decoded | raw bytes |
| httpx 0.28.1, Python 3.13.15 · bare install | raw bytes | raw bytes |
| httpx 0.28.1, Python 3.14.7 · bare install | raw bytes | raw bytes |
| httpx 0.28.1, Python 3.14.7 · + `brotli` | raw bytes | decoded |
| aiohttp 3.14.3, Python 3.13.15 · bare install | error | error |
| aiohttp 3.14.3, Python 3.14.7 · bare install | decoded | error |
| Go 1.27.1 · copied header | raw bytes | raw bytes |
| fetch, Node.js 22.23.3 · copied header | raw bytes | — |
| node:http, Node.js 22.23.3 · copied header | raw bytes | — |
| fetch, Node.js 24.21.0 · copied header | decoded | — |
| node:http, Node.js 24.21.0 · copied header | raw bytes | — |
| fetch, Node.js 26.10.0 · copied header | decoded | — |
| node:http, Node.js 26.10.0 · copied header | raw bytes | — |
| PHP 8.5.8 ext-curl (libcurl 8.20.0) · copied header | raw bytes | raw bytes |
| Guzzle 8.2.0 · copied header | decoded | decoded |
| Guzzle 8.2.0 StreamHandler · copied header | raw bytes | — |
| Java 25.0.4.1 HttpClient · copied header | raw bytes | — |

### A hand-set header and decoding

| Client | Sent | Your code got |
|---|---|---|
| curl 8.7.1 (macOS) · `-H 'Accept-Encoding: gzip'` | `gzip` | still `gzip`, 17,750 B |
| curl 8.22.0 (Homebrew) · `-H 'Accept-Encoding: gzip'` | `gzip` | still `gzip`, 17,750 B |
| Go 1.27.1 · `req.Header.Set("Accept-Encoding", "gzip")` | `gzip` | still `gzip`, 17,750 B |
| PHP 8.5.8 ext-curl (libcurl 8.20.0) · `CURLOPT_HTTPHEADER`, `Accept-Encoding: gzip` | `gzip` | still `gzip`, 17,750 B |
| Laravel Http 13.33.0 · `withHeaders(['Accept-Encoding' => 'gzip'])` | `gzip` | decoded, 100,129 B |
| Guzzle 8.2.0 · `'decode_content' => false` + `Accept-Encoding: gzip` | `gzip` | still `gzip`, 17,750 B |
| Java 25.0.4.1 HttpClient · `Accept-Encoding: gzip`, no `GZIPInputStream` | `gzip` | still `gzip`, 17,750 B |

### Exact error text

As recorded, except that loopback URLs and ports are shortened to …

- 3 record(s), for example `pit-curl-system-compressed-decoders--forced-br`, `pit-curl-system-compressed-decoders--forced-zstd`:

  ```text
  curl: (61) Unrecognized content encoding type. libcurl understands deflate, gzip content encodings.
  ```

- 5 record(s), for example `pit-py313-aiohttp-bare-decoders--forced-br`, `pit-py313-aiohttp-zstd-decoders--forced-br`:

  ```text
  aiohttp.client_exceptions.ClientResponseError: 400, message='Can not decode content-encoding: brotli (br). Please install `Brotli`', url='…'
  ```

- 5 record(s), for example `pit-py313-aiohttp-bare-decoders--forced-zstd`, `pit-py313-aiohttp-bare-decoders--forced-zstd-multiframe`:

  ```text
  aiohttp.client_exceptions.ClientResponseError: 400, message='Can not decode content-encoding: zstandard (zstd). Please install `backports.zstd`', url='…'
  ```

### HEAD and Range requests

| Client | Request | Sent |
|---|---|---|
| Go 1.27.1 | GET with `Range: bytes=0-` | (none) |
| Go 1.27.1 | HEAD | (none) |
| fetch, Node.js 22.23.3 | GET with `Range: bytes=0-` | `identity` |
| fetch, Node.js 24.21.0 | GET with `Range: bytes=0-` | `identity` |
| fetch, Node.js 26.10.0 | GET with `Range: bytes=0-` | `identity` |

### What an HTTP proxy can see

| Client | http:// through the proxy | https:// through CONNECT |
|---|---|---|
| curl 8.22.0 (Homebrew) · default | proxy saw no header | not visible |
| curl 8.22.0 (Homebrew) · `--compressed` | proxy saw `deflate, gzip, br, zstd` | not visible |
| Wget 1.25.0 · default | proxy saw `identity` | not visible |
| Requests 2.34.2, Python 3.14.7 · bare install | proxy saw `gzip, deflate, zstd` | not visible |
| urllib.request, Python 3.14.7 · standard library | proxy saw `identity` | not visible |
| fetch, Node.js 22.23.3 · default | sent through CONNECT: not visible | not visible |
| fetch, Node.js 24.21.0 · default | sent through CONNECT: not visible | not visible |
| fetch, Node.js 26.10.0 · default | proxy saw `gzip, deflate` | not visible |
| node:http, Node.js 26.10.0 · default | proxy saw no header | not visible |
| axios 1.20.0, Node.js 26.10.0 · default | proxy saw `gzip, compress, deflate, br` | not visible |
| Go 1.27.1 · default | proxy saw `gzip` | not visible |
| PHP 8.5.8 ext-curl (libcurl 8.20.0) · default | proxy saw no header | not visible |
| Guzzle 8.2.0 · default | proxy saw no header | not visible |
| Java 25.0.4.1 HttpClient · default | proxy saw no header | not visible |
| Guzzle 8.2.0 StreamHandler · default, http:// through a proxy, no `request_fulluri` | request failed at the proxy (see below) | — |

### Certificate checks stayed on

The same https requests without the lab CA. Every client refused the certificate. Loopback URLs and ports are shortened to …

| Client | Result without the lab CA |
|---|---|
| curl 8.7.1 (macOS) | `curl failed to verify the legitimacy of the server and therefore could not \| establish a secure connection …` |
| curl 8.22.0 (Homebrew) | `curl failed to verify the legitimacy of the server and therefore could not \| establish a secure connection …` |
| Wget 1.25.0 | `ERROR: cannot verify 127.0.0.1's certificate, issued by ‘CN=ipvolt compression lab local CA (throwaway)’: \|…` |
| urllib.request, Python 3.14.7 | `urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to…` |
| http.client, Python 3.14.7 | `ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get loc…` |
| Requests 2.34.2, Python 3.14.7 | `requests.exceptions.SSLError: HTTPSConnectionPool(host='127.0.0.1', port=…): Max retries exceeded with url:…` |
| httpx 0.28.1, Python 3.14.7 | `httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer …` |
| aiohttp 3.14.3, Python 3.14.7 | `aiohttp.client_exceptions.ClientConnectorCertificateError: Cannot connect to host 127.0.0.1:… ssl:True [SSL…` |
| Scrapy 2.19.0, Python 3.14.7 | `scrapy.exceptions.DownloadFailedError: [<twisted.python.failure.Failure OpenSSL.SSL.Error: [('SSL routines'…` |
| Go 1.27.1 | `*url.Error: Get "…": tls: failed to verify certificate: x509: “127.0.0.1” certificate is not trusted` |
| fetch, Node.js 22.23.3 | `TypeError: fetch failed (cause: Error UNABLE_TO_VERIFY_LEAF_SIGNATURE unable to verify the first certificate)` |
| fetch, Node.js 24.21.0 | `TypeError: fetch failed (cause: Error UNABLE_TO_VERIFY_LEAF_SIGNATURE unable to verify the first certificat…` |
| fetch, Node.js 26.10.0 | `TypeError: fetch failed (cause: Error UNABLE_TO_VERIFY_LEAF_SIGNATURE unable to verify the first certificat…` |
| node:https, Node.js 24.21.0 | `Error: unable to verify the first certificate; if the root CA is installed locally, try running Node.js wit…` |
| axios 1.20.0, Node.js 24.21.0 | `Error: unable to verify the first certificate; if the root CA is installed locally, try running Node.js wit…` |
| got 16.0.0, Node.js 24.21.0 | `RequestError: unable to verify the first certificate; if the root CA is installed locally, try running Node…` |
| PHP 8.5.8 ext-curl (libcurl 8.20.0) | `curl_exec failed: (60) SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)` |
| Guzzle 8.2.0 | `GuzzleHttp\Exception\ConnectException: cURL error 60: SSL certificate OpenSSL verify result: unable to get …` |
| Guzzle 8.2.0 StreamHandler | `GuzzleHttp\Exception\ConnectException: Error creating resource: [message] fopen(): SSL operation failed wit…` |
| Laravel Http 13.33.0 | `Illuminate\Http\Client\ConnectionException: cURL error 60: SSL certificate OpenSSL verify result: unable to…` |
| Java 25.0.4.1 HttpClient | `javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider…` |

## Verify snippets

Each published snippet ran unchanged except that `https://example.com/` became a loopback URL.

| Snippet | Client | Printed | Matches the origin log |
|---|---|---|---|
| `curl-verify.sh` | curl 8.7.1 | `100129 ` / `17750 gzip` | yes |
| `curl-verify.sh` | curl 8.22.0 | `100129 ` / `19298 zstd` | yes |
| `httpx-verify.py` | Python 3.14.7 httpx==0.28.1 | `gzip 17750 100129` | yes |
| `httpx-verify.py` | Python 3.14.7 httpx==0.28.1 zstandard==0.25.0 | `zstd 19298 100129` | yes |
| `requests-verify.py` | Python 3.13.15 requests==2.34.2 urllib3==2.8.0 | `gzip 17750` | yes |
| `requests-verify.py` | Python 3.14.7 requests==2.34.2 urllib3==2.8.0 | `zstd 19298` | yes |
| `node-fetch-verify.mjs` | Node.js v22.23.3 (undici 6.28.1, OpenSSL 3.5.8) | `sent accept-encoding: br, gzip, deflate` / `content-encoding br decoded bytes 100129` | yes |
| `node-fetch-verify.mjs` | Node.js v24.21.0 (undici 7.29.1, OpenSSL 3.5.8) | `sent accept-encoding: br, gzip, deflate` / `content-encoding br decoded bytes 100129` | yes |
| `node-fetch-verify.mjs` | Node.js v26.10.0 (undici 8.10.2, OpenSSL 3.5.8) | `sent accept-encoding: br, gzip, deflate, zstd` / `content-encoding zstd decoded bytes 100129` | yes |
| `node-fetch-bytes.mjs` | Node.js v22.23.3 (undici 6.28.1, OpenSSL 3.5.8) | `content-encoding br wire body bytes 18451 decoded bytes 100129` | yes |
| `node-fetch-bytes.mjs` | Node.js v24.21.0 (undici 7.29.1, OpenSSL 3.5.8) | `content-encoding br wire body bytes 18451 decoded bytes 100129` | yes |
| `node-fetch-bytes.mjs` | Node.js v26.10.0 (undici 8.10.2, OpenSSL 3.5.8) | `content-encoding zstd wire body bytes 19298 decoded bytes 100129` | yes |
| `node-http-verify.mjs` | Node.js v22.23.3 (undici 6.28.1, OpenSSL 3.5.8) | `sent accept-encoding (none)` / `content-encoding (none) body bytes 100129` | yes |
| `node-http-verify.mjs` | Node.js v24.21.0 (undici 7.29.1, OpenSSL 3.5.8) | `sent accept-encoding (none)` / `content-encoding (none) body bytes 100129` | yes |
| `node-http-verify.mjs` | Node.js v26.10.0 (undici 8.10.2, OpenSSL 3.5.8) | `sent accept-encoding (none)` / `content-encoding (none) body bytes 100129` | yes |
| `go-verify.go` | go-verify binary: go1.27.1 | `uncompressed=true content-encoding="" content-length=-1 decoded-bytes=100129` | yes |

`%header{}` in curl -w needs curl `7.84.0` or later (source-read from the man page shipped with curl 8.22.0; no older curl was run).

## Not run, dropped and reviewed anomalies

| Item | Status | Why |
|---|---|---|
| curl_cffi 0.16.3 | dropped | Optional in the brief; no cell was built. The lab makes no impersonation claims. |
| Apache HttpClient 4.5.10 (and OkHttp, Spring WebClient) | not run | No Java library beyond java.net.http.HttpClient was run. A 2021 third-party capture of Apache HttpClient 4.5.10 sending 'gzip,deflate' exists (Server Fault 1058430); it may be cited only as that. |
| Symfony HttpClient, PHP file_get_contents | not run | Only ext-curl, Guzzle (both handlers) and Laravel Http were run for PHP. |
| Browsers (Chromium, Firefox, WebKit) | not run | Out of scope for this lab; browsers negotiate compression themselves. |
| curl older than 7.84.0 | source-read | Only curl 8.7.1 and 8.22.0 were run. The 7.84.0 minimum for -w '%header\{name\}' is source-read from the curl 8.22.0 man page (see snippet_runs.curl_header_write_out). |
| HTTP/2 and HTTP/3 | not run | The origin offers HTTP/1.1 only (ALPN http/1.1), so every client stayed on HTTP/1.1. |
| Linux and Windows hosts | not run | All cells ran on macOS 15.7.4 arm64. Other builds of curl, Wget, PHP or Python can differ. |
| Corpus cells for Python libraries, Wget, Node.js 22, PHP and Java | not run | Corpus cells ran for curl (both builds), Node.js 24 and 26 fetch, and Go; the others use the fixture only. |
| Resource timing through an http:// proxy (absolute-form), and for axios, got, node-fetch or node:http | not run | Resource-timing cells cover Node.js built-in fetch only, on direct http://, direct https:// and https:// through CONNECT. Node.js 22 and 24 fetch tunnel http:// targets through CONNECT anyway (see reviewed_anomalies). |
| aiohttp 3.14.3 with backports.zstd on Python 3.14, or with both Brotli and backports.zstd | not run | Python 3.14 has compression.zstd in the standard library, which aiohttp uses there without an extra package; the run covers bare, +Brotli and (on 3.13) +backports.zstd installs separately. |
| Java feature releases after 25 (for example 27) | not run | Java ran on Temurin 25.0.4.1+1 (the newest LTS build on the run date) only. |
| Other PHP and libcurl builds | not run | PHP ran as the static-php-cli 8.5.8 build with its bundled libcurl 8.20.0 (php.net's newest 8.5.x was 8.5.11 on the run date). ext-curl sends what its libcurl build supports. |

### Anomalies the runner flagged, reviewed

| Record | Flag | Review |
|---|---|---|
| `node22-fetch--absolute-form` | `proxy saw ['CONNECT'], expected absolute-form` | Node.js 22.23.3 built-in fetch with NODE_USE_ENV_PROXY=1 and HTTP_PROXY opened a CONNECT tunnel for an http:// URL instead of sending an absolute-form request (extra.undici_sent and extra.proxy_modes_seen). The origin still received 'gzip, deflate'. Recorded as a client behaviour, not a harness failure: on this path the proxy cannot read the header. |
| `node24-fetch--absolute-form` | `proxy saw ['CONNECT'], expected absolute-form` | Same as node22-fetch--absolute-form on Node.js 24.21.0 (undici 7.29.1). Node.js 26.10.0 (node26-fetch--absolute-form) sent absolute-form and the proxy saw the header. |
| `pit-guzzle-stream-http-proxy-origin-form` | `origin saw no request` | Guzzle 8.2.0 StreamHandler sent an origin-form request line to an http:// proxy (no request_fulluri); the lab proxy requires absolute-form, answered 400 and forwarded nothing. A framing quirk, not a compression result; guzzle-stream-default-fulluri is the working cell. |
