Bloque II · Módulo 3 · Unidad 2.4
Respuestas, errores y contrato observable
Controlar representación pública, estados, headers y errores consistentes sin filtrar datos internos ni degradar el diagnóstico.
1. El contrato observable#
Un cliente no observa tus clases internas. Observa método, URL, status code, headers, media type y body. Un endpoint es correcto cuando esos elementos expresan de forma coherente el resultado y permanecen compatibles con lo documentado.
Separar contratos de entrada, dominio temporal y salida evita que un cambio interno publique accidentalmente campos nuevos.
2. Filtrar y validar con response_model#
from pydantic import BaseModel
class RecordInternal(BaseModel):
id: str
subject: str
owner_email: str
audit_token: str
class RecordPublic(BaseModel):
id: str
subject: str
@app.get("/expedientes/{record_id}", response_model=RecordPublic)
def read_record(record_id: str):
return RecordInternal(
id=record_id,
subject="Alta interna",
owner_email="owner@example.invalid",
audit_token="internal-only",
)
FastAPI usa response_model para documentar, serializar, validar y filtrar la salida. El cliente recibe solo id y subject. Esto no reemplaza una revisión de seguridad, pero convierte la representación pública en una lista permitida.
| Frontera | Fallo típico | Responsable | Respuesta |
|---|---|---|---|
| Request model | El cliente envía un tipo inválido. | Cliente. | 422 con ubicación útil. |
| Regla de negocio | Referencia duplicada. | Estado/operación. | 409 con error público. |
| Response model | La aplicación devuelve un dato incompatible. | Servidor. | Error interno; corregir implementación. |
3. Status code y headers#
from fastapi import Response, status
@app.post(
"/expedientes",
response_model=RecordPublic,
status_code=status.HTTP_201_CREATED,
)
def create_record(payload: RecordCreate, response: Response):
created = create_in_memory(payload)
response.headers["Location"] = f"/expedientes/{created.id}"
return created
201 Created comunica creación y Location identifica el recurso creado. Declarar status_code en el decorador lo incorpora a OpenAPI. Modificar el objeto Response inyectado permite añadir headers sin renunciar al filtrado del modelo.
4. Errores de negocio consistentes#
HTTPException interrumpe la operación y produce una respuesta:
from fastapi import HTTPException
if reference_exists(payload.reference):
raise HTTPException(
status_code=409,
detail={
"code": "record_reference_conflict",
"message": "The record reference is already in use",
},
)
Para evitar que unas operaciones devuelvan texto y otras diccionarios distintos, define un modelo:
class ErrorDetail(BaseModel):
code: str
message: str
field: str | None = None
class ErrorEnvelope(BaseModel):
error: ErrorDetail
Puedes crear una excepción de dominio pequeña y un manejador global que siempre produzca ErrorEnvelope. El dominio expresa el problema; el manejador decide el transporte HTTP.
5. Personalizar errores de validación sin perder señal#
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def validation_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
issues = [
{
"location": list(error["loc"]),
"code": error["type"],
"message": error["msg"],
}
for error in exc.errors()
]
return JSONResponse(
status_code=422,
content={
"error": {
"code": "request_validation_failed",
"message": "The request does not satisfy the contract",
"issues": issues,
}
},
)
La transformación conserva loc, type y un mensaje útil, pero omite la entrada cruda si podría contener secretos. No incluyas request.body() ni str(exc) sin una política explícita.
6. La operación también se documenta#
@app.post(
"/expedientes",
response_model=RecordPublic,
status_code=201,
summary="Create an internal record",
operation_id="createRecord",
tags=["records"],
responses={
409: {
"model": ErrorEnvelope,
"description": "Reference already exists",
}
},
)
def create_record(payload: RecordCreate):
...
El decorador anterior alimenta un documento con estructura OAS real. Esta vista YAML acotada muestra la raíz y los objetos principales que debes poder localizar:
openapi: 3.1.0
info:
title: API interna de expedientes
version: 0.1.0
paths:
/expedientes:
post:
summary: Crear un expediente interno
operationId: createRecord
tags: [records]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RecordCreate"
responses:
"201":
description: Expediente creado
headers:
Location:
description: URI del expediente creado
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/RecordPublic"
"409":
description: La referencia ya existe
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
components:
schemas:
RecordCreate:
type: object
required: [reference, subject, priority]
properties:
reference:
type: string
subject:
type: string
priority:
type: integer
minimum: 1
maximum: 5
RecordPublic:
type: object
required: [id, subject]
properties:
id:
type: string
subject:
type: string
ErrorEnvelope:
type: object
required: [error]
properties:
error:
$ref: "#/components/schemas/ErrorDetail"
ErrorDetail:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: string
Las claves fijas permanecen en inglés porque forman parte del vocabulario
case-sensitive de OpenAPI; title, summary y description contienen texto
humano y pueden localizarse. FastAPI añadirá además la respuesta de validación
422 cuando la operación acepte entrada validable.
Revisa /openapi.json, no solo /docs. El documento debe mostrar request body, respuesta 201, esquema público, 409 y 422. operation_id debe ser único y estable si clientes generan código.
7. Cuándo una Response directa cambia el contrato#
from fastapi.responses import PlainTextResponse
@app.get("/health/raw", response_class=PlainTextResponse)
def raw_health():
return "ok"
Una Response directa permite controlar body, media type, cookies y headers. Pero si devuelves un objeto Response construido manualmente, FastAPI no aplica sobre ese body el mismo pipeline de conversión y filtrado de un response_model.
Úsala cuando la representación no sea JSON o necesites control explícito. Para JSON de dominio, prefiere tipos y modelos declarativos.
8. Criterios de dominio#
- El modelo público filtra campos internos.
- Creación devuelve
201y unLocationcoherente. - Errores de negocio tienen código estable y estado defendible.
- El manejador de validación conserva ubicación sin filtrar input sensible.
- OpenAPI describe éxito y errores relevantes.
- Una respuesta manual se usa de forma consciente y documentada.
- No se confunden fallos de entrada con bugs de salida.
Prácticas relacionadas#
- EX-B2-10 · Detectar una fuga de salida
- EX-B2-11 · Unificar el error de negocio
- EX-B2-12 · Preservar localización
- EX-B2-13 · Creación y Location
- EX-B2-14 · Revisar OpenAPI
- MP-B2-R · Validador de expedientes