Exercise 1 — Defenses
- With the naive endpoint, a curl with
{"status": "SUCCEEDED"}marks other people's payments: hacked in 30 seconds. The signature is the barrier. - HMAC over the RAW body (the exact bytes): if you verify over a re-serialized
request.data, a spacing/key-order change invalidates the signature (or worse, accepts variants).compare_digestis constant-time:==short-circuits and allows theoretical timing attacks. - Signature of another body → the HMAC doesn't match (the timestamp is inside the signed material): 401. Old timestamp → 400: an intercepted payload is worthless after the window (5 min: the balance between unsynchronized clocks and the attack window; the provider signs the timestamp so you can't move it).
Exercise 2 — ACK + queue
- Synchronous: 3s+ response. With the provider's 5s timeout you're at the limit; if your DB slows down, the provider marks failure and retries while you're still processing → duplicates and storm.
- ACK in <100ms: validate + enqueue (or a thread spawn in simulation). The real processing lives in the worker with its own idempotency.
exists(gateway_event_id)before creating: 1 record, 3×200. The duplicate is NOT an error: it is expected (at-least-once).
Exercise 3 — Out of order
- If your handler blindly applied
status = payload["status"], the confirmed payment would go back to PENDING: the state machine (00b) prevents it —SUCCEEDED → PENDINGis not a legal transition and raises InvalidTransition. - To the legal event: 200 (processed). To the illegal one: 200 with "ignored" (log the rejected transition) — answering 4xx/5xx would trigger endless provider retries for an event that simply arrived late and must not be applied. Exception: an UNKNOWN event (a new format you don't recognize) does deserve 4xx so the provider flags it... or 200 with a log: it depends on the provider; you document it in their checklist (§4).
Exercise 4 — Outgoing
- A deliveries table with
attempts,next_retry_atand backoff 1m/5m/30m/2h/6h; the worker (or celery beat) queries "pending with next_retry <= now()". - 503 → failed attempt → attempts+1 and next_retry scheduled; after 5, FAILED (and alert/panel: the organizer isn't receiving, which is a support incident, not silence).
- 410 Gone = the subscription is dead by the receiver's decision: deactivate the endpoint (flag) and notify the organizer through another channel. Retrying forever against 410 burns your queue — that is why you distinguish 5xx (retry) from 410 (don't retry) and auth 4xx (don't retry; check credentials).
Professor's summary
- Zero trust: signature over raw body + anti-replay window + idempotency by event_id.
- Fast ACK + worker: the provider retries with a timeout; your slowness becomes a storm.
- The state machine absorbs disorder; illegal events get ignored with 200 and a log.
- What you demand on receive, you deliver on send: signature, backoff retries, and knowing when NOT to retry (410).
The APIs module closes. Next: Lesson 18 — Sessions vs tokens (the Security module).