◐ ai-teacher

🐍 Backend with Python · Chapter 2

HTTP properly: methods, status codes, headers, idempotency

Networks drop replies, so clients retry. Whether that retry books one cab or two is decided by the HTTP method you picked and the status codes you send.

Study this lesson free in the app →

The basics

HTTP is the agreed language between client and backend. Four parts of it carry meaning that every client, proxy and library relies on.

The problem it solves: clients, caches and retry libraries you'll never see make decisions from the method and the status code alone, so if those are wrong, retries double-charge people and caches serve stale data.

The mental model: the method is the verb (read, create, replace, delete), the status code is the outcome in one number, and headers are the envelope notes (what format, who's asking, where the new thing lives).

Words you must know 5

Method
GET reads, POST creates or triggers, PUT replaces at a known address, PATCH changes part, DELETE removes.
Safe
the method asks for no change on the server: GET, HEAD, OPTIONS.
Idempotent
sending it 5 times leaves the server the same as sending it once: all safe methods plus PUT and DELETE. POST and PATCH are not.
Status code
2xx it worked, 3xx look elsewhere, 4xx your request is wrong, 5xx the server failed.
Header
a Name: value line of metadata, like Content-Type: application/json or Location: /orders/981.

Explained

Friday, 7:52 PM, Bengaluru metro

Ananya pays ₹1,240 for groceries as the train enters a tunnel. The app sends POST /payments. The server charges her bank in 900 ms and writes the row. The reply never arrives: the network is gone.

The app waits 10 seconds, sees a timeout, and does what retry libraries do: sends the same POST again. The server, seeing a brand-new request, charges her again. Two debits, one order.

Nobody wrote a bug in the handler. The bug is that the app retried a method that isn't idempotent, and the server gave it no way to recognise a repeat. The fix is ten lines and a header, and it's in this chapter.

Two ways to order at a chai stall

You shout "Bhaiya, make it 2 chai for table 4." The boy didn't hear, so you shout it again. Table 4 still gets 2 chai. That is PUT: you state the final result, so repeating it is harmless.

Your friend shouts "2 more chai for table 4!" twice. Now table 4 has 4 extra chai and a bigger bill. That is POST: each message is a new action.

"Cancel table 4's order" twice? Still cancelled, nothing more happens. That is DELETE.

"What's on the menu?" changes nothing at all, however many times you ask. That is GET: safe, not just idempotent.

Same words, same table. Whether shouting twice is a problem depends entirely on which kind of sentence you used.

Methods, per RFC 9110

| Method | Use it to | Safe | Idempotent | Body | | --- | --- | --- | --- | --- | | GET | read a resource | ✅ | ✅ | none | | HEAD | GET without the body (size, freshness) | ✅ | ✅ | none | | OPTIONS | ask what's allowed (CORS preflight) | ✅ | ✅ | none | | POST | create a child, or trigger an action | ❌ | ❌ | yes | | PUT | create or replace at this exact URL | ❌ | ✅ | yes | | PATCH | change part of a resource | ❌ | ❌ | yes | | DELETE | remove | ❌ | ✅ | rarely |

Safe = the client asks for no state change, so crawlers, link previewers and prefetchers will happily call it. Idempotent = the effect on the server of N identical requests equals the effect of one. It says nothing about the response being identical: the second DELETE may well get 404.

Retry rule that follows: a client or proxy may automatically retry idempotent requests after a dropped connection. POST and PATCH need an idempotency key first.

Status codes you'll actually send

The rule: 4xx = the client should change the request; 5xx = retrying the same request later might work.

Headers: the notes on the envelope

POST /orders HTTP/1.1                      ← method, path, version
Host: api.chaiwala.in                      ← which site (one IP, many sites)
Content-Type: application/json             ← how to read MY body
Accept: application/json                   ← what I can read back
Authorization: Bearer eyJhbGciOi…          ← who I am (be-008)
Idempotency-Key: 4b1e-9c2d-k7f3a           ← "this is the same order if I retry"
Content-Length: 26
                                           ← blank line = headers over
{"item": "chai", "qty": 2}

HTTP/1.1 201 Created                       ← outcome first
Location: /orders/981                      ← where the new thing lives
Content-Type: application/json
Content-Length: 37

{"id": 981, "item": "chai", "qty": 2}

Content-Type describes the body of the message it's in. Accept describes what the sender wants back. Location on a 201 saves the client a guess. Idempotency-Key isn't in RFC 9110: it's an IETF draft that Stripe and most payment APIs already use.

Predict: three PUTs, three POSTs

carts = {}

@app.put("/carts/{cid}/items/chai")
def set_chai(cid: int, qty: int):
    carts.setdefault(cid, {})["chai"] = qty        # set, not add
    return carts[cid]

@app.post("/carts/{cid}/items/chai")
def add_chai(cid: int, qty: int):
    cart = carts.setdefault(cid, {})
    cart["chai"] = cart.get("chai", 0) + qty       # add
    return cart

for _ in range(3): r = client.put("/carts/1/items/chai?qty=2")
print(r.json())
for _ in range(3): r = client.post("/carts/2/items/chai?qty=2")
print(r.json())

How an idempotency key stops the double charge

In production seen_keys is a database table with a unique constraint on the key, entries expire after about 24 hours, and two requests with the same key arriving at once must not both pass line 8 (be-006 covers the transaction that guarantees it).

HTTP mistakes that reach production

Words to know

Safe methodA method whose semantics request no state change on the server: GET, HEAD, OPTIONS, TRACE.
Idempotent methodN identical requests have the same intended effect on the server as one: the safe methods plus PUT and DELETE.
Idempotency keyA unique id the client sends with a non-idempotent request so the server can detect a retry and replay the first response.
201 CreatedThe request made a new resource; the Location header should give its URL.
401 vs 403401: no valid credentials, authenticate. 403: credentials understood, permission refused.
422 Unprocessable ContentThe request is well-formed but its values fail validation. FastAPI's default for validation errors.
Location headerThe URL of the resource a 201 created, or of the target of a 3xx redirect.
Retry-AfterHeader on 429 or 503 telling the client how long to wait before trying again.

In two minutes

Test yourself

1. What does it mean for an HTTP method to be idempotent?

  1. Repeating the request returns byte-identical responses
  2. Repeating the request N times leaves the server in the same state as sending it once
  3. The request changes nothing on the server
  4. The response can be cached
Show the answer

Repeating the request N times leaves the server in the same state as sending it once — Idempotency is about effect on server state. 'Changes nothing' is the stronger property, safe. Identical responses aren't required: DELETE then DELETE gives 204 then 404.

2. Which methods does RFC 9110 define as safe?

  1. GET, HEAD, OPTIONS, TRACE
  2. GET, PUT, DELETE
  3. GET and POST
  4. Every method except DELETE
Show the answer

GET, HEAD, OPTIONS, TRACE — Safe means the client requests no state change. PUT and DELETE are idempotent but not safe: they change state, just repeatably.

3. Why is it fine for a client to auto-retry a timed-out PUT but dangerous to auto-retry a POST?

  1. PUT requests are smaller
  2. Servers ignore duplicate PUTs automatically
  3. POST bodies are encrypted
  4. PUT replaces the resource at a known URL, so a repeat lands on the same state; a repeated POST can create a second resource
Show the answer

PUT replaces the resource at a known URL, so a repeat lands on the same state; a repeated POST can create a second resource — The client can't know whether the first request was applied. With PUT it doesn't matter; with POST you may get two orders. Servers don't dedupe anything 'automatically': that's what idempotency keys are for.

4. What is the difference between 401 and 403?

  1. 401 is for APIs, 403 for web pages
  2. 401 is temporary, 403 is permanent
  3. 401: we don't know who you are (missing or bad credentials); 403: we know, and you're not allowed
  4. They mean the same; pick either
Show the answer

401: we don't know who you are (missing or bad credentials); 403: we know, and you're not allowed — Despite its name, 401 'Unauthorized' is about authentication, and it should carry a WWW-Authenticate header. Sending 401 to a logged-in user without permission makes clients re-login in a loop.

5. What happens when a client sends POST /menu to a FastAPI app that only defines @app.get("/menu")?

  1. 404 Not Found
  2. 405 Method Not Allowed, with an `Allow: GET` header
  3. The GET handler runs
  4. 422 Unprocessable Content
Show the answer

405 Method Not Allowed, with an `Allow: GET` header — The path exists, the method doesn't. FastAPI answers 405 with Allow: GET and body {"detail":"Method Not Allowed"}. 404 would wrongly claim nothing lives at that path.

6. How should a successful POST /orders that created order 981 respond?

  1. 201 Created with `Location: /orders/981` and the order in the body
  2. 200 with an empty body
  3. 204 No Content with the id in the body
  4. 302 redirect to /orders
Show the answer

201 Created with `Location: /orders/981` and the order in the body — 201 says something new exists, and Location says where. A 204 must not have a body, so the id would be lost. 200 isn't wrong, but it throws away information the client can use.

There are 10 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →