Part 7 of 8 · 4 min read
Exercises
Try each exercise before opening its hint.
Easy
Easy 1: Read the wire (20 minutes)
Run curl -i https://httpbin.org/get (or any public JSON API; if that site is down, try https://api.github.com) and label the status line, three headers and the body. Then run curl -i https://httpbin.org/status/429 (or ask any service for a page that does not exist). Write what your code should do on each response.
Done when: you can say which of your answers would differ if the status were 404 or 503.
Hint
curl -i prints headers. -s hides progress, | head -20 trims the output.
Easy 2: Sort the status codes (15 minutes)
Sort these into "retry", "fix the request" and "stop and tell a human": 200, 400, 401, 403, 404, 408, 409, 422, 429, 500, 502, 503. Add one sentence of reasoning for any you were unsure about.
Done when: every code is placed, and 429 and 503 are in the retry group.
Easy 3: Run the example tests (20 minutes)
Install httpx and pytest, go to code/http-client and run pytest. Read each test. Change RETRYABLE_STATUS to remove 429 and watch which test fails.
Done when: you can explain each of the six tests in one sentence.
Medium
Medium 1: Add a total deadline (40 minutes)
Right now five attempts could take a very long time. Add a deadline: float parameter (seconds for the whole call). Stop retrying and raise when the deadline would be exceeded by the next sleep. Write a test with a fake clock.
Done when: the test proves no sleep starts after the deadline.
Hint
use time.monotonic() for measuring, never time.time(), because the wall clock can jump.
Medium 2: Retry a POST safely (40 minutes)
Write post_json(client, url, payload) that generates one Idempotency-Key (a UUID) before the first attempt and sends the same key on every retry. Test with a mock server that fails once with 503 then succeeds, and check that both requests carried the same key.
Done when: the test passes and you can say what would go wrong with a new key each time.
Hint
Python's uuid.uuid4() makes the key. Create it outside the retry loop.
Medium 3: Cap and extend Retry-After (35 minutes)
The example already sleeps for the number of seconds in Retry-After. Improve it in two ways: never sleep more than a cap you choose (a header saying 3600 must not make your agent wait an hour), and also accept the date form of the header (for example Wed, 21 Oct 2026 07:28:00 GMT). Test a numeric value above the cap, a date, and a missing header.
Done when: all three tests pass.
Hint
email.utils.parsedate_to_datetime parses the date form. Compute the seconds from now, and never return a negative number.
Hard
Hard 1: Fetch everything (60 minutes)
Pick a real paginated API (for example the GitHub REST API list of issues for a public repository). Use paginate from the example to fetch all items, with a timeout and a polite delay. Print the count and the time taken. Handle the 403 or 429 that appears when you hit the unauthenticated rate limit, and print the X-RateLimit-Remaining header.
Done when: your script completes against real data and fails gracefully when limited.
Hint
GitHub returns the next page in a Link header, not a next_cursor. Read its docs (module 8) and adapt the loop.
Hard 2: A circuit breaker (60 minutes)
Write a small Breaker class: after three consecutive failures it refuses calls for 30 seconds (using an injected clock), then lets one trial call through. Wrap the client with it and test the closed, open and half-open states.
Done when: a test walks through all three states without sleeping for real.
Hint
store failures, opened_at and a clock function; the half-open state is just "opened_at is old enough, allow one call".
Hard 3: Cursor and Link pagination in one function (60 minutes)
Write iter_pages(client, url) that handles both styles from section 5: a next_cursor field in the JSON body and a Link: <...>; rel="next" header. Test it against two mock servers, one of each style, and one that returns an empty last page.
Done when: one function passes all three tests and never loops forever.
Hint
httpx.Response.links parses the Link header into a dictionary keyed by rel.