Módulo 1 · Fundamentos que sostienen todo

Lección 01 — HTTP a fondo

Métodos, códigos de estado con criterio, CORS, cookies seguras, caché y TLS — verificado con curl.

Publicada
En esta lección
  1. Ejercicio 1 — Anatomía con
  2. Ejercicio 2 — Diseño del contrato
  3. Ejercicio 3 — Corrige el código
  4. Ejercicio 4 — CORS en la práctica
  5. Ejercicio 5 — Cookies seguras
  6. Ejercicio 6 — ETag y 304
  7. Ejercicio 7 — Razona como diseñador
  8. Ejercicio 8 — HTTPS y TLS
  9. Resumen del profesor

No leas esto sin haber entregado antes tus intentos. Los errores que no se cometen no se aprenden.


Ejercicio 1 — Anatomía con curl -v

  1. Línea de petición: GET /api/health/ HTTP/1.1; línea de estado: HTTP/1.1 200 OK. Tres cabeceras (cualquiera de): Date, Server, Content-Type, Content-Length, Vary, Allow... Cuerpo: {"status":"ok","service":"ticketflow"}.
  2. Con Accept: text/html, DRF negocia contenido: como el endpoint usa api_view y el renderer HTML viene activo por defecto en DEBUG, devolvería el HTML navegable de DRF (o un 406 si no hay renderer compatible). La cabecera Accept guía qué representación quieres del mismo recurso.

Ejercicio 2 — Diseño del contrato

Petición (caso feliz):

http
POST /api/events/42/reserve HTTP/1.1
Host: ticketflow.example.com
Content-Type: application/json
Authorization: Bearer <token>
Idempotency-Key: 7f3c9a2e          ← clave anti-duplicado si el cliente reintenta

{"seats": [1051, 1052]}

Respuesta feliz:

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

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

Respuesta con conflicto:

http
HTTP/1.1 409 Conflict

{"code": "seat_already_reserved", "detail": "El asiento 1051 ya está reservado"}

Comentario: 409 Conflict es el código para choques de estado (asiento ocupado). 422 también se defiende, pero 409 comunica mejor "tu petición es válida, pero el mundo cambió". Veremos esto a fondo en la Lección 10 (transacciones: el asiento se decide en una transacción con bloqueo, no por "comprobar y reservar").

Ejercicio 3 — Corrige el código

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"])   # si se usa como cancelación vía DELETE, DELETE es idempotente
def cancel_reservation(request, pk):
    try:
        reservation = Reservation.objects.get(pk=pk)
    except Reservation.DoesNotExist:
        return Response({"detail": "No existe"}, status=status.HTTP_404_NOT_FOUND)
    reservation.status = "cancelled"
    reservation.save()
    return Response({"status": "cancelled"}, status=status.HTTP_200_OK)

Fallos originales:

  1. 200 implícito: sin status=, DRF responde 200. Si el endpoint actualiza el estado, 200 es aceptable; pero si el diseño fuera "DELETE de la reserva", lo correcto es 204 No Content. El error real es la ambigüedad: no dejar el código al azar.
  2. 500 si no existe: objects.get lanza DoesNotExist → Django lo traduce a 500 en una vista API salvo que lo captures o uses get_object_or_404. Un recurso inexistente es 404, no 500.

Bonus senior: cancelar debería ser idempotente: cancelar dos veces la misma reserva no debería fallar (devolver 200 con estado cancelled o 204). Lo retomamos en la Lección 14.

Ejercicio 4 — CORS en la práctica

  1. Con Origin: http://localhost:5173: respuesta 200 OK (preflight) con cabeceras como Access-Control-Allow-Origin: http://localhost:5173, Access-Control-Allow-Methods:...POST..., Access-Control-Allow-Headers: authorization, content-type. El navegador considera la petición autorizada.
  2. Con Origin: http://evil.example: django-cors-headers no emite las cabeceras Access-Control-* (y la petición puede responder 200 pero sin autorización CORS). El navegador la bloqueará. Nota fina: el bloqueo lo hace el navegador, no el servidor; curl nunca se bloquea.
  3. Tras añadir Idempotency-Key a CORS_ALLOW_HEADERS, la respuesta del preflight la incluye en Access-Control-Allow-Headers. Sin eso, el navegador rechazaría cualquier petición real que la envíe.

Lo que acabas de probar: el mecanismo exacto que usa un frontend en localhost:5173 para poder consumir tu API de localhost:8000.

Ejercicio 5 — Cookies seguras

  1. Set-Cookie: sessionid=...; Path=/; SameSite=Lax (más HttpOnly si es de sesión de Django; con el admin verás también csrftoken).
  2. Con SESSION_COOKIE_SECURE = True, la cookie no viaja por HTTP plano: curl http://... no la verá en la respuesta (o el navegador la descartaría). Solo se envía por HTTPS.
  3. - HttpOnly: JS no puede leerla → XSS no roba la sesión.
  4. Secure: no viaja en claro → MitM no la captura.
  5. SameSite=Lax/Strict: el navegador no la adjunta en peticiones cross-site → mitiga CSRF.

Ejercicio 6 — ETag y 304

  1. Primera respuesta: 200 con ETag: "<hash>" y Cache-Control: max-age=60,... (además Expires, Last-Modified según el helper).
  2. Con If-None-Match: "<tu-etag>": respuesta 304 Not Modified y cuerpo vacío.
  3. Ahorro: el servidor no serializa ni transfiere el cuerpo (ni el cliente lo descarga); se intercambian solo cabeceras. En listados grandes es una ganancia enorme. Contra-partido: la validación implica comparar ETags (aquí trivial; con contenido dinámico hay que calcularlo bien para no regenerar todo).

Ejercicio 7 — Razona como diseñador

  1. PATCH /reservations/1051 con {"expires_at": "+10min"} (o subrecurso /reservations/1051/extension) → 200. Es una modificación parcial del recurso, no creación.
  2. PATCH /events/42 con {"status": "cancelled"} → 200 (o PUT si se reemplaza la representación completa; PUT sería idempotente). Marcar cancelado no "borra" nada: no es DELETE.
  3. GET /events/42/availability → 200 (cacheable con Cache-Control corto o ETag; la disponibilidad cambia, usa TTL bajo).
  4. POST /checkout → 202 Accepted con cuerpo tipo {"job_id": "...", "status": "queued"}. El proceso se encola (Lección 06) y la confirmación llega por polling o webhook. 202 comunica exactamente "aceptado, aún no listo".

Ejercicio 8 — HTTPS y TLS

  1. Confidencialidad (anula eavesdropper en la red: Wi-Fi abierto, ISP), integridad (anula a quien altera paquetes/proxy malicioso) y autenticidad del servidor (anula phishing/MITM con certificado falso: la CA es la que te dice que el servidor es quien dice ser).
  2. TLS 1.3 reduce el handshake a 1 round-trip (y 0 con session resumption/TLS 1.3 + TCP ya establecida o QUIC): menos RTT antes de la primera petición = TTFB menor en cada conexión nueva. Además elimina cifrados legacy.
  3. El TLS termina en Nginx: el cliente cifra hasta Nginx; de Nginx a Gunicorn/Django viaja (normalmente) en claro dentro de la red privada. "Terminación TLS" es eso. Implicación: el tráfico interno debe estar protegido por otros medios (red privada, mTLS o TLS interno) — detalle de arquitectura que retomamos en la Lección 02 y en el Módulo 9.

Resumen del profesor

  • HTTP es simple: método + ruta + cabeceras + cuerpo; lo difícil es la disciplina semántica (códigos correctos, idempotencia, seguridad en cabeceras).
  • Un buen backend no solo responde JSON: responde con el código correcto, las cabeceras correctas y cuerpos consistentes.
  • Todo lo de hoy se verifica con curl -i/-v. Hazlo siempre: las cabeceras mienten menos que la documentación.