Module 1 · Foundations that hold everything up

Lesson 01 — HTTP in depth

Methods, status codes with intent, CORS, secure cookies, caching and TLS — verified with curl.

Published
In this lesson
  1. Exercise 1 — Anatomy with
  2. Exercise 2 — Contract design
  3. Exercise 3 — Fix the code
  4. Exercise 4 — CORS in practice
  5. Exercise 5 — Secure cookies
  6. Exercise 6 — ETag and 304
  7. Exercise 7 — Reason like a designer
  8. Exercise 8 — HTTPS and TLS
  9. Professor's summary

Don't read this before submitting your attempts. The mistakes you don't make are the ones you don't learn from.


Exercise 1 — Anatomy with curl -v

  1. Request line: GET /api/health/ HTTP/1.1; status line: HTTP/1.1 200 OK. Three headers (any of): Date, Server, Content-Type, Content-Length, Vary, Allow... Body: {"status":"ok","service":"ticketflow"}.
  2. With Accept: text/html, DRF negotiates content: since the endpoint uses api_view and the HTML renderer is enabled by default in DEBUG, it would return DRF's browsable HTML (or a 406 if no compatible renderer). The Accept header guides which representation of the same resource you want.

Exercise 2 — Contract design

Request (happy path):

http
POST /api/events/42/reserve HTTP/1.1
Host: ticketflow.example.com
Content-Type: application/json
Authorization: Bearer <token>
Idempotency-Key: 7f3c9a2e          ← anti-duplicate key in case the client retries

{"seats": [1051, 1052]}

Happy response:

http
HTTP/1.1 201 Created
Location: /api/reservations/9172/
Content-Type: application/json

{"id": 9172, "status": "pending_payment", "seats": [1051, 1052], "expires_at": "..."}

Conflict response:

http
HTTP/1.1 409 Conflict

{"code": "seat_already_reserved", "detail": "Seat 1051 is already reserved"}

Comment: 409 Conflict is the code for state clashes (seat taken). 422 is also defensible, but 409 communicates better "your request is valid, but the world changed". We'll dig into this in Lesson 10 (transactions: the seat is decided inside a transaction with a lock, not by "check and reserve").

Exercise 3 — Fix the code

python
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.exceptions import NotFound

@api_view(["POST", "DELETE"])   # if used as DELETE-style cancellation, DELETE is idempotent
def cancel_reservation(request, pk):
    try:
        reservation = Reservation.objects.get(pk=pk)
    except Reservation.DoesNotExist:
        return Response({"detail": "Not found"}, status=status.HTTP_404_NOT_FOUND)
    reservation.status = "cancelled"
    reservation.save()
    return Response({"status": "cancelled"}, status=status.HTTP_200_OK)

Original mistakes:

  1. Implicit 200: without status=, DRF answers 200. If the endpoint updates state, 200 is acceptable; but if the design were "DELETE the reservation", the right answer is 204 No Content. The real mistake is ambiguity: never leave the status code to chance.
  2. 500 when it doesn't exist: objects.get raises DoesNotExist → Django turns it into a 500 in an API view unless you catch it or use get_object_or_404. A missing resource is 404, not 500.

Senior bonus: cancelling should be idempotent: cancelling the same reservation twice shouldn't fail (return 200 with cancelled state or 204). We'll return to this in Lesson 14.

Exercise 4 — CORS in practice

  1. With Origin: http://localhost:5173: a 200 OK (preflight) response with headers like Access-Control-Allow-Origin: http://localhost:5173, Access-Control-Allow-Methods:...POST..., Access-Control-Allow-Headers: authorization, content-type. The browser considers the request authorized.
  2. With Origin: http://evil.example: django-cors-headers doesn't emit the Access-Control-* headers (the request may still answer 200 but without CORS authorization). The browser will block it. Fine note: the block is done by the browser, not the server; curl never blocks.
  3. After adding Idempotency-Key to CORS_ALLOW_HEADERS, the preflight response includes it in Access-Control-Allow-Headers. Without that, the browser would reject any real request that sends it.

What you just tested: the exact mechanism a frontend on localhost:5173 uses to consume your API on localhost:8000.

Exercise 5 — Secure cookies

  1. Set-Cookie: sessionid=...; Path=/; SameSite=Lax (plus HttpOnly for Django's session cookie; via the admin you'll also see csrftoken).
  2. With SESSION_COOKIE_SECURE = True, the cookie does not travel over plain HTTP: curl http://... won't see it in the response (and a browser would discard it). It's only sent over HTTPS.
  3. - HttpOnly: JS can't read it → XSS can't steal the session.
  4. Secure: never sent in the clear → MitM can't capture it.
  5. SameSite=Lax/Strict: the browser won't attach it on cross-site requests → mitigates CSRF.

Exercise 6 — ETag and 304

  1. First response: 200 with ETag: "<hash>" and Cache-Control: max-age=60,... (plus Expires, Last-Modified depending on the helper).
  2. With If-None-Match: "<your-etag>": a 304 Not Modified response and an empty body.
  3. Savings: the server neither serializes nor transfers the body (and the client doesn't download it); only headers are exchanged. On large listings that's a huge win. Trade-off: validation implies comparing ETags (trivial here; with dynamic content you must compute them well, or you'll regenerate everything anyway).

Exercise 7 — Reason like a designer

  1. PATCH /reservations/1051 with {"expires_at": "+10min"} (or a subresource /reservations/1051/extension) → 200. It's a partial modification of the resource, not a creation.
  2. PATCH /events/42 with {"status": "cancelled"} → 200 (or PUT if you replace the whole representation; PUT would be idempotent). Marking as cancelled deletes nothing: it is not DELETE.
  3. GET /events/42/availability → 200 (cacheable with a short Cache-Control or an ETag; availability changes, so use a low TTL).
  4. POST /checkout → 202 Accepted with a body like {"job_id": "...", "status": "queued"}. The process is queued (Lesson 06) and confirmation arrives via polling or webhook. 202 communicates exactly "accepted, not ready yet".

Exercise 8 — HTTPS and TLS

  1. Confidentiality (defeats a network eavesdropper: open Wi-Fi, ISP), integrity (defeats whoever alters packets / a malicious proxy) and server authenticity (defeats phishing/MITM with a fake certificate: the CA is who tells you the server is who it claims to be).
  2. TLS 1.3 reduces the handshake to 1 round-trip (and 0 with session resumption/TLS 1.3 over an established connection or QUIC): fewer RTTs before the first request = lower TTFB on every new connection. It also removes legacy ciphers.
  3. TLS terminates at Nginx: the client encrypts up to Nginx; from Nginx to Gunicorn/Django traffic usually travels in the clear inside the private network. That's "TLS termination". Implication: internal traffic must be protected by other means (private network, mTLS or internal TLS) — an architecture detail we'll revisit in Lesson 02 and Module 9.

Professor's summary

  • HTTP is simple: method + path + headers + body; the hard part is semantic discipline (correct codes, idempotency, header security).
  • A good backend doesn't just answer JSON: it answers with the right status code, the right headers and consistent bodies.
  • Everything from today is verified with curl -i/-v. Always do it: headers lie less than documentation.