M4.C3 — Middleware + CORS¶
Pre-requisitos: M4.C2 — Body + query + headers. Sabés recibir y validar todo lo que el cliente puede mandar.
Objetivo: cubrir todo lo que pasa alrededor del handler — logging, autenticación, rate limiting, observability, y CORS (para que tu API la consuman frontends en otro dominio).
Por qué importa: el handler es solo "qué hacer cuando llega una request válida". El middleware modela el resto del ciclo de vida HTTP — lo que pasa antes del handler (auth, validaciones cross-cutting), después del handler (logging, modificar response, agregar headers) o envolviendo la invocación (timing, decisión condicional de continuar). Sin middleware, todo eso se repite en cada handler.
Cross-link: Guía cap 17 — Middleware y CORS.
Mapa del cap¶
flowchart LR
A[Request] --> B["Middleware 1: Pre"]
B --> C["Middleware 2: Wrap"]
C --> D["Middleware 3: Wrap"]
D --> E[Handler]
E --> F["Middleware 3: Wrap salida"]
F --> G["Middleware 2: Wrap salida"]
G --> H["Middleware: Post"]
H --> I[Response final]
J[CORS] -.special.-> A
J -.preflight.-> I
Tres clases de middleware + CORS como caso especial.
Paso 1 — @middleware(fn) — la idea base¶
Decorás un handler con @middleware(funcion) antes del decorator
de ruta. La fn se ejecuta como parte del ciclo de la request:
@server(3000)
fn main() => 0
fn logger(req: Request) {
// Sin return → la cadena continúa al handler
}
@middleware(logger)
@get("/")
fn index() -> Str => "ok"
Reglas básicas:
- Los
@middleware(...)van antes del decorator de ruta (@get/@post/...). - Cada uno recibe un primer arg de tipo
Request. - El orden importa: el
@middleware(...)más arriba corre primero.
Request es un tipo built-in del runtime con fields:
| Field | Tipo | Para qué |
|---|---|---|
method |
Str |
"GET" / "POST" / ... |
path |
Str |
URL path (sin query) |
headers |
Map<Str, Str> |
Headers de la request (keys lowercase) |
Paso 2 — Las tres clases de middleware¶
Fitz tiene tres modelos según la firma de la fn:
| Aridad | Tipo del 2do param | Kind | Cuándo corre |
|---|---|---|---|
| 1 | (solo Request) |
Pre | ANTES del handler (gate-only) |
| 2 | Response |
Post | DESPUÉS del handler |
| 2 | Fn() -> Response |
Wrap | ENVUELVE la invocación |
El runtime infiere el kind automáticamente desde la firma.
Pre (gate)¶
fn auth(req: Request) {
if (req.headers.has("authorization")) {
return null // continúa la chain
}
return 401 {"error": "auth required"}
}
@middleware(auth)
@get("/admin")
fn admin() -> Str => "datos administrativos"
Comportamiento:
| Acción del middleware | Efecto |
|---|---|
return null o sin return |
Continúa al próximo middleware / handler |
return <status> { ... } |
Corta la chain, el handler nunca corre |
| Cualquier otro retorno | Error claro al registrar |
Demo:
curl -i http://127.0.0.1:3000/admin
# HTTP/1.1 401 Unauthorized
# {"error":"auth required"}
curl -i http://127.0.0.1:3000/admin -H "Authorization: x"
# HTTP/1.1 200 OK
# "datos administrativos"
Use cases típicos: - Autenticación - Rate limiting - Logging básico (request recibida) - Validación cross-cutting (todos los handlers que necesitan un header X)
Post (post-process)¶
fn add_server_header(req: Request, resp: Response) -> Response {
// Modificá la response o devolvé una nueva
return resp
}
@middleware(add_server_header)
@get("/")
fn index() -> Str => "hola"
Response es un tipo built-in opaco (no podés ver sus
fields directamente). Hoy podés:
- Recibirla del handler.
- Devolverla tal cual.
- Devolver una response distinta con
return <status> { ... }.
fn rewrite(req: Request, resp: Response) -> Response {
if (req.path == "/secret") {
return 200 {"rewritten": true}
}
return resp
}
Use cases típicos: - Logging de response (status, tiempo, body size) - Inyectar headers de seguridad - Rewriting de errores
Limitación: handlers -> Result<T> + post-process middleware
no compila en fitz build todavía (deuda residual). En
fitz run funciona end-to-end.
Wrap (envoltura con next)¶
El segundo param es un callable Fn() -> Response. El
middleware decide cuándo (o si) invocar next():
fn timing(req: Request, next: Fn() -> Response) -> Response {
// Acá podríamos medir tiempo antes
let r = next() // ejecuta el resto: wraps, handler, posts
// Acá podríamos medir tiempo después
return r
}
@middleware(timing)
@get("/api")
fn api() -> Str => "data"
Más útil: gate condicional con response wrapping:
fn auth_gate(req: Request, next: Fn() -> Response) -> Response {
let token = req.headers.get("x-auth")
match token {
Ok(_) => next() // ejecuta el handler
Err(_) => return 401 {"error": "auth required"}
}
}
| Cuándo Wrap > Pre | Por qué |
|---|---|
| Medir tiempo de toda la chain | Pre no ve la response |
| Decisión condicional de invocar handler | Pre solo gate-only |
| Envolver la response (cambiar body después) | Pre no devuelve response |
| Tres niveles de mw + handler todo medido | Pre+Post no anida correctamente |
Limitación: Wrap solo en fitz run. fitz build rechaza
con error claro:
✗ middleware wrap-style 'timing' requiere `fitz run`.
`fitz build` no soporta wrap todavía (sub-paso futuro). Usá
Pre + Post como alternativa, o `fitz run`.
Paso 3 — Apilar múltiples middlewares¶
Los @middleware(...) se apilan top-down:
@middleware(logger) // corre primero
@middleware(auth) // corre segundo
@middleware(rate_limit) // corre tercero
@get("/api")
fn api() -> Str => "ok"
Orden de ejecución para una request entrante:
logger(req)→ null → continúaauth(req)→ null o cortarrate_limit(req)→ null o cortarapi()→ handler corre
Para post-process middlewares el orden de salida es inverso:
Ejecución:
outer_postse anota para correr al finalinner_postse anota para correr al final- Handler corre
inner_post(req, resp)corre primeroouter_post(req, resp)corre después
Como una stack — el más cercano al handler corre primero al salir.
Mezclar pre + post + wrap¶
@middleware(logger) // Pre
@middleware(timing) // Wrap
@middleware(auth) // Pre
@middleware(add_headers) // Post
@get("/api")
fn api() -> Str => "ok"
Flow:
logger(req)
timing(req, next):
auth(req)
handler api()
respuesta
add_headers(req, resp)
retorno de timing
respuesta final
Paso 4 — CORS — para frontends en otro dominio¶
El problema: si tu API corre en api.miempresa.com y tu
frontend Vue/React corre en app.miempresa.com, el browser
bloquea las requests del frontend por la same-origin
policy.
La solución estándar: el server responde headers
Access-Control-Allow-* autorizando explícitamente al frontend
a hacer requests cross-origin. Eso es CORS.
En Fitz: cors(...) es un built-in que se aplica como
middleware:
cors() sin args usa defaults permisivos (todo origen
permitido). Para producción querés restringir — más abajo.
Qué hace cors(...) por dentro¶
Cuando aplicás cors(...):
- Registra un handler OPTIONS automático para el mismo
path. Una request preflight
OPTIONS /api/itemsresponde 204 No Content con los headersAccess-Control-Allow-*configurados. - Inyecta headers en las responses reales del handler
(
GET /api/items,POST ...). Esto vale también para errores (500/400/etc.) — sin eso, el browser tapa el error real con un "CORS error" en consola que es indescifrable.
Demo del preflight:
curl -i -X OPTIONS http://127.0.0.1:3000/api/items \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: GET"
# HTTP/1.1 204 No Content
# access-control-allow-origin: *
# access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS
# access-control-allow-headers: content-type, authorization
Demo del request real:
curl -i http://127.0.0.1:3000/api/items \
-H "Origin: https://app.example.com"
# HTTP/1.1 200 OK
# content-type: application/json
# access-control-allow-origin: *
# ["uno","dos"]
Configuración custom¶
@middleware(cors({
"allow_origin": "https://app.example.com",
"allow_methods": ["GET", "POST"],
"allow_headers": ["content-type", "authorization", "x-trace-id"],
"max_age": 3600
}))
@get("/api/items")
fn list_items() -> List<Str> => ["uno", "dos"]
Keys soportadas:
| Key | Tipo | Default | Para qué |
|---|---|---|---|
allow_origin |
Str o "echo" o List<Str> |
"*" |
Origen(es) permitidos |
allow_methods |
List<Str> |
["GET","POST","PUT","DELETE","OPTIONS"] |
Métodos HTTP que el browser puede usar |
allow_headers |
List<Str> |
["content-type","authorization"] |
Headers custom permitidos |
max_age |
Int |
(ausente) | Cuánto cachea el browser el preflight (segundos) |
Modos de allow_origin¶
| Valor | Comportamiento |
|---|---|
"*" (default) |
Cualquier origen. Incompatible con cookies/auth según el spec CORS |
"https://app.example.com" (literal) |
Solo ese origen exacto |
["https://a.com", "https://b.com"] (List) |
Echo del Origin si está en la lista, si no NO emite el header |
"echo" (especial) |
Echo del Origin recibido sin filtro. Útil para dev local |
// Producción con cookies → echo selectivo
@middleware(cors({
"allow_origin": ["https://app.miempresa.com", "https://staging.miempresa.com"]
}))
@get("/api/me")
fn me() -> Str => "session info"
// Dev local — cualquier puerto del local
@middleware(cors({"allow_origin": "echo"}))
@get("/api")
fn api() -> Str => "dev mode"
Restricciones de cors(...)¶
- Máximo uno por ruta. Apilar dos da error al registrar.
- Convive sin problema con user-fn middlewares:
- En
fitz build,cors(...)se evalúa en build-time: el codegen emite unstatic __FITZ_CORS_*con los headers precomputados y un handler de preflight dedicado. Cero overhead por request.
Paso 5 — Patrones comunes con middleware¶
Auth con JWT (preview de M5)¶
fn auth_jwt(req: Request) {
let token = req.headers.get("authorization")
match token {
Err(_) => return 401 {"error": "missing auth header"},
Ok(t) => {
// Validar JWT — cubierto en detalle en M5
// jwt.decode(t, "secret") ...
return null
}
}
}
Auth nativa de Fitz con @authenticated y jwt built-in se
cubre en M5.C3 del curso.
Logging estructurado¶
fn log_request(req: Request) {
print("→ {req.method} {req.path}")
}
fn log_response(req: Request, resp: Response) -> Response {
print("← {req.method} {req.path}: <status>")
return resp
}
@middleware(log_request)
@middleware(log_response)
@get("/x")
fn x() -> Str => "ok"
Para logs production-ready (timestamps, trace IDs, JSON estructurado), usás el toolkit del lenguaje + algo de helpers.
Rate limiting básico¶
let rate_state: Map<Str, Int> = {}
fn rate_limit(req: Request) {
let ip = req.headers.get("x-real-ip")
let key = match ip {
Ok(v) => v
Err(_) => "unknown"
}
let count = rate_state.get(key)
let current = match count {
Ok(n) => n
Err(_) => 0
}
if (current >= 100) {
return 429 {"error": "rate limit exceeded"}
}
rate_state[key] = current + 1
return null
}
@middleware(rate_limit)
@get("/api")
fn api() -> Str => "ok"
Limitación: el state global se resetea cada vez que reiniciás el server. Para rate limiting real querés Redis o un store externo.
Timing observability (Wrap)¶
fn timing(req: Request, next: Fn() -> Response) -> Response {
// Acá medirías el tiempo antes y después
// Hoy `time.now()` no es built-in — usaría una helper de FFI futura
let r = next()
return r
}
Cuando aterricen time.now() y métricas built-in en el lenguaje
(post Fase 12), esto se vuelve trivial.
Paso 6 — Subset compilable a binario (fitz build)¶
| Feature | fitz run |
fitz build |
|---|---|---|
@middleware(fn) Pre |
✅ | ✅ |
@middleware(fn) Post (handler sin Result) |
✅ | ✅ |
@middleware(fn) Post (handler -> Result<T>) |
✅ | ❌ deuda |
@middleware(fn) Wrap |
✅ | ❌ usa fitz run |
cors() defaults |
✅ | ✅ (build-time, cero overhead) |
cors({...}) con todos los modos |
✅ | ✅ |
| Múltiples middlewares apilados | ✅ | ✅ |
| Preflight OPTIONS automático | ✅ | ✅ |
Paso 7 — Demo end-to-end¶
@server(3000, "0.0.0.0")
fn main() => 0
// --- Pre: logger global ---
fn logger(req: Request) {
print("→ {req.method} {req.path}")
}
// --- Pre: auth simple por header ---
fn auth(req: Request) {
if (req.headers.has("authorization")) {
return null
}
return 401 {"error": "auth required"}
}
// --- Public endpoint ---
@middleware(logger)
@middleware(cors())
@get("/health")
fn health() -> Str => "ok"
// --- Protected endpoint ---
@middleware(logger)
@middleware(cors({
"allow_origin": "https://app.miempresa.com",
"allow_methods": ["GET", "POST"]
}))
@middleware(auth)
@get("/me")
fn me() -> Str => "usuario actual"
// --- Public POST con CORS ---
@middleware(logger)
@middleware(cors())
@post("/items")
fn create_item() -> Str => "creado"
Probalo:
# Health pública
curl http://127.0.0.1:3000/health
# "ok"
# /me sin auth → 401
curl -i http://127.0.0.1:3000/me
# HTTP/1.1 401 Unauthorized
# /me con auth → 200
curl http://127.0.0.1:3000/me -H "Authorization: x"
# "usuario actual"
# Preflight OPTIONS desde un browser ficticio
curl -i -X OPTIONS http://127.0.0.1:3000/items \
-H "Origin: https://app.miempresa.com" \
-H "Access-Control-Request-Method: POST"
# HTTP/1.1 204 No Content
# access-control-allow-origin: *
Validación¶
-
@middleware(logger)apilado sobre un handler corre ANTES del handler en cada request. - Un middleware Pre que devuelve
return 401 {...}corta la chain — el handler no corre. - Middleware sin return o con
return nulldeja seguir. -
cors()sin args responde 204 al preflight OPTIONS. -
cors({"allow_origin": "..."})emite el headerAccess-Control-Allow-Originen cada response. - Múltiples
@middleware(...)corren en orden top-down para Pre, y bottom-up para Post. -
fitz buildproduce binario que ejecuta Pre y Post mws con la misma semántica quefitz run(Wrap rechazado).
Troubleshooting¶
"expected Request but found something else"¶
El primer param de un middleware tiene que tipar como Request.
Si declarás otro tipo:
Error claro al registrar.
Middleware Pre que cortocircuita aunque debería continuar¶
Solo se permite return null (continuar) o return <status> {...}
(cortar). Cualquier otro retorno es error al registrar.
"ya hay un cors() registrado sobre esta ruta"¶
Apilaste dos cors(...):
Máximo uno por ruta. Si necesitás varias políticas, partí en handlers distintos.
CORS no funciona desde el browser pero curl funciona¶
CORS solo aplica a requests desde un browser que activa la same-origin policy. Curl no respeta CORS. Para debuggear:
- Abrí DevTools del browser → Network → mirar el preflight OPTIONS y verificá que devuelva 204 con los headers que esperás.
- Verificá que el
Originque manda el browser está en elallow_origindel server. - Si usás cookies o
Authorization,allow_origin: "*"no funciona — usá un valor específico o"echo".
Wrap middleware no compila con fitz build¶
Hoy Wrap solo corre interpretado. Para producción:
- Convertí el Wrap a Pre + Post si la lógica permite.
- Usá
fitz run(acepta producción si el throughput alcanza). - Esperá el sub-paso futuro que cierra esta deuda.
Post mw después de un handler -> Result<T> no compila¶
Workaround: convertí el handler a -> T y manejá el error
adentro con return <status> {...}. O usá fitz run.
Lo que sigue¶
Ya tenés un toolkit HTTP completo — rutas, body, query,
headers, middleware, CORS. El próximo cap cubre algo único
de Fitz que ahorra horas en cada proyecto: OpenAPI 3.1
autogenerado desde tus decoradores + tipos custom + anotaciones
+ una UI interactiva en /docs. Cero pip install fastapi[all]
ni springdoc-openapi-ui.