A dependable client needs both data validation and a stopping policy. The capstone joins those concerns over supplied attempt events. Each event has elapsed milliseconds and either no response or a structured response. The function returns data or a precise failure, the number of consumed events, and planned delays. It never opens a connection, sleeps or reads a clock. These traces test decisions; they do not prove live network readiness.
Timeout is an observation about waiting, not proof that a server did nothing.
Our exact fixture contract accepts elapsed_ms == timeout_ms and treats only
a greater elapsed value as a timeout. A late supplied 200 is ignored. Otherwise
received responses pass through the earlier decoder, while a missing response
is transport. Successful HEAD and 204/205 results need no body parsing.
Retry only the declared transient outcomes: transport failure, timeout, or received 429/502/503/504. The method must be GET, HEAD, OPTIONS, PUT or DELETE. POST/PATCH stop because this policy has no endpoint-specific replay guarantee. A malformed JSON success is a terminal data error, rather than a reason to hammer the endpoint with identical requests.
Three budgets answer different questions: timeout_ms limits the supplied
elapsed time for one attempt, max_attempts limits consumed events, and
max_retry_ms limits the sum of planned waits. Real library timeout parameters
can cover particular blocking operations rather than a complete end-to-end
deadline; do not assume this synthetic elapsed field implements their behavior.
attempt = 2
fallback_ms = 100 * 2 ** (attempt - 1)
waited_ms, budget_ms = 100, 300
print(fallback_ms) # 200
print(waited_ms + fallback_ms <= budget_ms) # True: equality is permitted
print(1001 > 1000, 1000 > 1000) # timeout, within budgetMissing Retry-After uses fallback delays 100, 200, 400, ... milliseconds.
A unique ASCII decimal Retry-After specifies seconds, so 2 replaces the
fallback with 2000 milliseconds. Validate ASCII digits rather than relying
on isdigit, which also recognizes other Unicode numerals. Zero is allowed.
A repeated, empty or malformed value stops with retry-after.
HTTP also permits an HTTP-date in Retry-After. This clock-free tool deliberately declines that form; treating an unhandled future date as zero would guess at the server's wait. A real implementation can support dates with an explicit clock seam and account for clock differences. A delay that exceeds the remaining wait budget stops before being appended; it is not silently shortened. Real clients often add jitter to avoid synchronized retries. This lab keeps fallbacks deterministic so exact policy outcomes are reproducible.
The trace can end while the policy still wants another attempt. Return
trace-exhausted with the planned delays retained, including a final plan
without a following event. Stop consuming events immediately after success,
a terminal data error or a budget decision. A successful page preserves its
opaque next cursor; the separate pagination lab handles bounded traversal.
Start with a valid 200, then add a 503, a server-directed delay, a timeout and a POST replay hazard. Test both sides of each budget boundary. The supplied helpers keep the build focused on orchestration; the bugs isolate timeout equality and unsafe automatic replay.
Use RFC 9110 sections 9.2.2 and 10.2.3 for idempotence and Retry-After, and Python's urllib.request reference for the distinction between an actual client API and this offline model.