Module 5 · Architecture and maintainable code

Lesson 26 — Centralized error handling

One consistent error format for the whole API, not 40 different responses.

Published
In this lesson
  1. Exercise 1 — The taxonomy
  2. Exercise 2 — The handler
  3. Exercise 3 — Actionable extra
  4. Exercise 4 — The healthy 500
  5. Exercise 5 — The client that programs against the contract
  6. Submit

Implement TicketFlow's error contract. No solutions.md before submitting.

Exercise 1 — The taxonomy

  1. List TicketFlow's domain exceptions with their status: seat taken (409), reservation expired (410), event sold out (409), payment declined (402 or 409 — decide and justify), gift card balance insufficient (409 or 422 — decide), not authenticated (401, DRF handles it).
  2. For each one write the complete problem+json (type, title, status, detail) exactly as the frontend's client would see it.

Exercise 2 — The handler

  1. Implement problem_handler with to_problem() and problem_response(), wire it in settings and make DRF's validation errors come out as problem+json (status 400, type .../validation-error, and errors with the field → messages dict).
  2. Write the contract test: POST to reservations with an invalid body → 400 with Content-Type application/problem+json and a type ending in validation-error.
  3. Migrate an OLDER view (the one still returning Response({"error":...})): delete its try/except, raise the domain exception, and verify with the test that the response didn't change (the test is 28's safety net).

Exercise 3 — Actionable extra

  1. Add extra to SeatUnavailable(conflicting_seats) and to ReservationExpired(retry_after) — in the latter case also the Retry-After header. Where does the handler inject them?
  2. Test: expired reservation → 410 with header Retry-After: 30 and body with retry_after: 30.

Exercise 4 — The healthy 500

  1. Force a 500 (a view raising ZeroDivisionError on a test endpoint). Verify: generic problem+json body WITHOUT stack, log with logger.exception and the trace_id present.
  2. Add the http_5xx_total{path} metric (in-memory or Redis counter, 46 formalizes it) incremented in the handler. Why in the handler and not in a separate middleware? (hint: one single place).
  3. Decide and write: should a 429 (23's rate limit) be logged with warning or is it noise? And a 401?

Exercise 5 — The client that programs against the contract

  1. Write a test that consumes the API with a fake HTTP client and applies ALL the rules: every 4xx/5xx error has an absolute URI type, a non-empty title, a status matching the HTTP status, trace_id present on 5xx. A single assert_problem(response) function.
  2. Paste it into 3 different tests (validation, domain, 500). How many lines did it save you versus writing the assertions by hand in each one?

Submit

Paste the taxonomy, the complete handler, the contract test and the 500 test. Next: Lesson 27 — 12-factor configuration.