En APIs asincronas, responder 202 Accepted suele parecer suficiente. El problema llega despues: QA quiere saber si el correo salio, soporte quiere contexto y backend termina mirando logs que no fueron pensados para humanos. Ese hueco no siempre rompe producción, pero si vuelve las pruebas mas lentas y medio cansadas.
Cuando una suite necesita generar correo desechable para validar registro, invitaciones o recovery, el objetivo no deberia ser solo "esperar a ver si llega algo". Prefiero exponer un recibo corto de entrega desde FastAPI. No es glamoroso, pero hace que la automatizacion y el debug de APIs sean bastante mas honestos.
Por que el 202 Accepted no le alcanza a QA
El 202 confirma que aceptaste trabajo, no que el email exista en un estado util para el equipo. Si el worker falla, reintenta o queda atascado, QA se queda sin pista clara. Ahi empiezan varias cosas feas:
- el test vuelve a consultar la bandeja una y otra vez
- alguien dispara el mismo flujo por segunda vez
- soporte ve "enviado" en un panel y "no recibido" en otro
- los bugs tardan mas porque nadie sabe en qué paso se dobló la cosa
Ese coste pega incluso en sistemas chicos. Segun el libro de SRE de Google, la observabilidad efectiva reduce tiempo de diagnostico porque hace visibles sintomas y causas con menos adivinanza source. No arregla la cola por si sola, pero evita muchas vueltas inutiles.
El recibo pequeno que cambia la conversacion
Lo que mejor me ha funcionado es guardar un recibo mínimo por intento de email. No un mega log, solo un objeto estable que QA, soporte y backend puedan leer sin interpretar eventos crudos.
Yo suelo incluir:
message_idstatusproviderlast_event_atattempt_count-
error_codecuando aplique
Con eso cambias la pregunta de "llegó o no llegó?" por "en qué estado real quedó?". Parece detalle menor, pero no lo es. El equipo deja de perseguir sombras y puede separar error de plantilla, retraso del proveedor, o un webhook que nunca aterrizó. A veces esa distinción sola ya te ahorra una mañana medio rota.
También queda mejor alineado con frontend. Si el producto ya trabaja un feedback estable al enviar emails, este recibo backend le da una base confiable a ese estado visual en lugar de puro optimismo.
Un ejemplo corto con FastAPI
No hace falta montar una arquitectura enorme para empezar. Este patrón cabe en un endpoint, una tabla y un worker pequeño:
from datetime import datetime, timezone
from enum import StrEnum
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from uuid import uuid4
app = FastAPI()
receipts: dict[str, dict] = {}
class ReceiptStatus(StrEnum):
QUEUED = "queued"
SENT = "sent"
FAILED = "failed"
class SendEmailRequest(BaseModel):
to: str
template: str
@app.post("/email/send")
def send_email(payload: SendEmailRequest):
message_id = str(uuid4())
receipts[message_id] = {
"message_id": message_id,
"status": ReceiptStatus.QUEUED,
"provider": "resend",
"attempt_count": 0,
"last_event_at": datetime.now(timezone.utc).isoformat(),
}
enqueue_email_job(message_id, payload.model_dump())
return {"message_id": message_id, "status": ReceiptStatus.QUEUED}
@app.post("/email/events/{message_id}")
def apply_email_event(message_id: str, status: ReceiptStatus):
receipt = receipts.get(message_id)
if not receipt:
raise HTTPException(status_code=404, detail="message not found")
receipt["status"] = status
receipt["attempt_count"] += 1
receipt["last_event_at"] = datetime.now(timezone.utc).isoformat()
return receipt
@app.get("/email/receipts/{message_id}")
def read_receipt(message_id: str):
receipt = receipts.get(message_id)
if not receipt:
raise HTTPException(status_code=404, detail="message not found")
return receipt
No usaria este diccionario fuera de una demo, obvio. Pero la idea queda clara: el recibo es una interfaz, no un efecto secundario escondido. Luego puedes moverlo a PostgreSQL, Redis o donde quieras, y el contrato sigue igual. Eso es lo que vuelve mas facil depurar.
Como lo pruebo sin volver fragil la suite
Aqui es donde muchos equipos se pasan de listos y sin querer empeoran todo. Si cada test espera el correo final, la suite se pone lenta y un poco caprichosa. Yo prefiero dos capas:
- pruebas de API que verifican el recibo y sus transiciones
- pocas pruebas end-to-end que verifican entrega real
Ese reparto deja a QA con evidencia util y mantiene bajo el ruido. Para pruebas de onboarding, por ejemplo, puedes combinar el recibo con cohortes claras y cohortes de onboarding sin ruido para no mezclar activaciones reales, staging y test internos.
Si en tus fixtures alguien escribió temp org mail para nombrar una bandeja temporal, no pasa nada. Lo importante es que ese alias no sea la única fuente de verdad. La fuente de verdad deberia ser el recibo con timestamps y cambios de estado. La bandeja confirma entrega; el recibo explica la historia. Esa diferencia parece chica, pero salva mucho tiempo cuando algo sale raro.
Preguntas frecuentes
¿Hace falta guardar cada evento del proveedor?
No siempre. Para muchos equipos basta con el ultimo estado visible y un contador de intentos. Si luego necesitas auditoria completa, la agregas. Empezar pequeño suele salir mejor, la verdad.
¿Esto reemplaza revisar la bandeja temporal?
No. La complementa. El recibo te dice qué cree tu sistema; la bandeja te dice si el mensaje fue observable desde afuera. Juntas funcionan mejor que por separado.
¿Cuanto detalle pongo en el recibo?
El minimo que permita decidir la siguiente accion sin abrir logs. Si QA puede ver queued, sent o failed con hora y proveedor, ya vas muy bien. Mas datos no siempre ayudan; a veces solo hacen ruido, y un poquito de ruido ya sobra bastante.
Top comments (0)