CodeRigor

FREE LESSON

Capstone: a deterministic API client policy

Combine response validation with bounded retries, timeout decisions and Retry-After budgets over supplied offline attempt traces.

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.

One attempt has a bounded outcome
start / 3
This is a policy over supplied events. It makes no requests and measures no real network timing.
One attempt has a bounded outcome
This is a policy over supplied events. It makes no requests and measures no real network timing.

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.

Python
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 budget

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

Planned waiting has its own budget
start / 3
A 503 plans 100 ms; a following 429 with Retry-After: 2 plans 2000 ms. No sleeps occur.
Planned waiting has its own budget
A 503 plans 100 ms; a following 429 with Retry-After: 2 plans 2000 ms. No sleeps occur.

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.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Focus on response statuses, JSON handling and timeout configuration in an actual client. The build instead evaluates supplied traces; its exact timeout, retry and waiting budgets are stated in the local contract. Requests is not installed or required by the grader.