Skip to content

Commit 43a97dc

Browse files
lesnik512claude
andcommitted
test: lock ResponseTooLargeError resilience semantics
Cap trips are not retried, do not trip the circuit breaker, and an over-cap retryable 5xx surfaces as ResponseTooLargeError (cap-wins). No prod change — these assert the behavior that falls out of the ClientError hierarchy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6aea224 commit 43a97dc

1 file changed

Lines changed: 70 additions & 0 deletions

File tree

tests/test_resilience_body_cap.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Resilience interaction with max_response_body_bytes.
2+
3+
ResponseTooLargeError is a non-status ClientError, so it must fall outside the
4+
retry/circuit-breaker failure classifications. These tests lock that behavior so
5+
a future refactor can't silently make a cap trip retryable or breaker-counting.
6+
"""
7+
8+
import httpx2
9+
import pytest
10+
11+
from httpware import AsyncClient, CircuitState, ResponseTooLargeError
12+
from httpware.middleware.resilience.circuit_breaker import AsyncCircuitBreaker
13+
from httpware.middleware.resilience.retry import AsyncRetry
14+
15+
16+
class _CountingHandler:
17+
"""Mock transport that counts calls and always returns the same response."""
18+
19+
def __init__(self, status: int, body: bytes) -> None:
20+
self.status = status
21+
self.body = body
22+
self.calls = 0
23+
24+
def __call__(self, request: httpx2.Request) -> httpx2.Response:
25+
self.calls += 1
26+
return httpx2.Response(self.status, content=self.body, request=request)
27+
28+
29+
def _client(handler: _CountingHandler, *, middleware: list[object], cap: int) -> AsyncClient:
30+
return AsyncClient(
31+
httpx2_client=httpx2.AsyncClient(transport=httpx2.MockTransport(handler)),
32+
middleware=middleware, # ty: ignore[invalid-argument-type]
33+
max_response_body_bytes=cap,
34+
)
35+
36+
37+
async def test_response_too_large_is_not_retried() -> None:
38+
handler = _CountingHandler(200, b"x" * 200)
39+
client = _client(handler, middleware=[AsyncRetry()], cap=10)
40+
request = client.build_request("GET", "https://example.test/x")
41+
with pytest.raises(ResponseTooLargeError):
42+
await client.send(request)
43+
assert handler.calls == 1 # not retried — a single terminal attempt
44+
await client.aclose()
45+
46+
47+
async def test_over_cap_retryable_5xx_surfaces_as_too_large_not_retried() -> None:
48+
# 503 is retryable, but the cap trips first: cap-wins / fail-hard.
49+
handler = _CountingHandler(503, b"x" * 200)
50+
client = _client(handler, middleware=[AsyncRetry()], cap=10)
51+
request = client.build_request("GET", "https://example.test/x")
52+
with pytest.raises(ResponseTooLargeError) as caught:
53+
await client.send(request)
54+
assert caught.value.status_code == 503 # noqa: PLR2004 — the retryable status, surfaced not retried
55+
assert handler.calls == 1
56+
await client.aclose()
57+
58+
59+
async def test_response_too_large_does_not_trip_circuit_breaker() -> None:
60+
# failure_threshold=1: one real failure would open the circuit; a cap trip must not.
61+
handler = _CountingHandler(500, b"x" * 200)
62+
breaker = AsyncCircuitBreaker(failure_threshold=1)
63+
client = _client(handler, middleware=[breaker], cap=10)
64+
request = client.build_request("GET", "https://example.test/x")
65+
for _ in range(3):
66+
with pytest.raises(ResponseTooLargeError):
67+
await client.send(request)
68+
assert breaker.state is CircuitState.CLOSED # neither success nor failure recorded
69+
assert handler.calls == 3 # noqa: PLR2004 — breaker never opened, every call reached the transport
70+
await client.aclose()

0 commit comments

Comments
 (0)