|
16 | 16 | from httpware.errors import ResponseTooLargeError |
17 | 17 |
|
18 | 18 |
|
19 | | -def _sync_stream(handler: object) -> tuple[httpx2.Client, httpx2.Response]: |
| 19 | +def _sync_stream(handler: object, method: str = "GET") -> tuple[httpx2.Client, httpx2.Response]: |
20 | 20 | client = httpx2.Client(transport=httpx2.MockTransport(handler)) # ty: ignore[invalid-argument-type] |
21 | | - request = client.build_request("GET", "https://example.test/x") |
| 21 | + request = client.build_request(method, "https://example.test/x") |
22 | 22 | return client, client.send(request, stream=True) |
23 | 23 |
|
24 | 24 |
|
25 | | -async def _async_stream(handler: object) -> tuple[httpx2.AsyncClient, httpx2.Response]: |
| 25 | +async def _async_stream(handler: object, method: str = "GET") -> tuple[httpx2.AsyncClient, httpx2.Response]: |
26 | 26 | client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) # ty: ignore[invalid-argument-type] |
27 | | - request = client.build_request("GET", "https://example.test/x") |
| 27 | + request = client.build_request(method, "https://example.test/x") |
28 | 28 | return client, await client.send(request, stream=True) |
29 | 29 |
|
30 | 30 |
|
@@ -81,6 +81,39 @@ def handler(request: httpx2.Request) -> httpx2.Response: # noqa: ARG001 |
81 | 81 | client.close() |
82 | 82 |
|
83 | 83 |
|
| 84 | +def test_read_capped_within_cap_gzip_returns_decoded_content() -> None: |
| 85 | + # Regression: rebuilt Response must not re-decompress already-decoded content. |
| 86 | + raw = gzip.compress(b"A" * 500) |
| 87 | + |
| 88 | + def handler(request: httpx2.Request) -> httpx2.Response: # noqa: ARG001 |
| 89 | + return httpx2.Response(200, headers={"content-encoding": "gzip"}, content=raw) |
| 90 | + |
| 91 | + client, resp = _sync_stream(handler) |
| 92 | + try: |
| 93 | + out = _read_capped(resp, 1_000_000, resp.request) |
| 94 | + assert out.content == b"A" * 500 # decoded, not re-gzipped/crashed |
| 95 | + assert "content-encoding" not in out.headers # stale wire header dropped |
| 96 | + assert out.headers["content-length"] == "500" # recomputed from decoded content |
| 97 | + finally: |
| 98 | + resp.close() |
| 99 | + client.close() |
| 100 | + |
| 101 | + |
| 102 | +def test_read_capped_head_with_large_declared_length_not_rejected() -> None: |
| 103 | + # Regression: a bodiless HEAD response buffers nothing and must not trip the cap. |
| 104 | + def handler(request: httpx2.Request) -> httpx2.Response: # noqa: ARG001 |
| 105 | + return httpx2.Response(200, headers={"content-length": "50000000"}) |
| 106 | + |
| 107 | + client, resp = _sync_stream(handler, method="HEAD") |
| 108 | + try: |
| 109 | + out = _read_capped(resp, 1000, resp.request) |
| 110 | + assert out.content == b"" |
| 111 | + assert out.headers["content-length"] == "50000000" # entity length preserved for HEAD |
| 112 | + finally: |
| 113 | + resp.close() |
| 114 | + client.close() |
| 115 | + |
| 116 | + |
84 | 117 | def test_read_capped_gzip_bomb_trips_on_decoded_bytes() -> None: |
85 | 118 | raw = gzip.compress(b"A" * 100_000) |
86 | 119 |
|
@@ -142,6 +175,37 @@ def handler(request: httpx2.Request) -> httpx2.Response: # noqa: ARG001 |
142 | 175 | await client.aclose() |
143 | 176 |
|
144 | 177 |
|
| 178 | +async def test_read_capped_async_within_cap_gzip_returns_decoded_content() -> None: |
| 179 | + raw = gzip.compress(b"A" * 500) |
| 180 | + |
| 181 | + def handler(request: httpx2.Request) -> httpx2.Response: # noqa: ARG001 |
| 182 | + return httpx2.Response(200, headers={"content-encoding": "gzip"}, content=raw) |
| 183 | + |
| 184 | + client, resp = await _async_stream(handler) |
| 185 | + try: |
| 186 | + out = await _read_capped_async(resp, 1_000_000, resp.request) |
| 187 | + assert out.content == b"A" * 500 |
| 188 | + assert "content-encoding" not in out.headers |
| 189 | + assert out.headers["content-length"] == "500" |
| 190 | + finally: |
| 191 | + await resp.aclose() |
| 192 | + await client.aclose() |
| 193 | + |
| 194 | + |
| 195 | +async def test_read_capped_async_head_with_large_declared_length_not_rejected() -> None: |
| 196 | + def handler(request: httpx2.Request) -> httpx2.Response: # noqa: ARG001 |
| 197 | + return httpx2.Response(200, headers={"content-length": "50000000"}) |
| 198 | + |
| 199 | + client, resp = await _async_stream(handler, method="HEAD") |
| 200 | + try: |
| 201 | + out = await _read_capped_async(resp, 1000, resp.request) |
| 202 | + assert out.content == b"" |
| 203 | + assert out.headers["content-length"] == "50000000" |
| 204 | + finally: |
| 205 | + await resp.aclose() |
| 206 | + await client.aclose() |
| 207 | + |
| 208 | + |
145 | 209 | async def test_read_capped_async_declared_over_cap() -> None: |
146 | 210 | body = b"x" * 200 |
147 | 211 |
|
|
0 commit comments