DEV Community

Olivia Cheng
Olivia Cheng

Posted on

FastAPI: eventos de email sin polling ciego

En un flujo de signup, el email suele parecer una tarea secundaria: crear la cuenta, enviar un enlace y esperar que aparezca en una bandeja de prueba. El problema llega cuando el test pregunta cada pocos segundos si ya hay un mensaje nuevo. Ese polling funciona al principio, pero pronto se vuelve lento, frágil y dificil de explicar.

He visto el mismo fallo en proyectos con Python: el endpoint responde bien, el worker envia el mensaje, pero la prueba mira la bandeja demasiado pronto. Luego se añade un sleep(3), despues otro reintento y, finalmente, un test que pasa en local pero falla en CI. No es que el correo sea misterioso. Falta un contrato claro entre el evento que produce el backend y la evidencia que consume la prueba.

El problema de esperar una bandeja

Un polling ciego mezcla varias preguntas distintas:

  1. ¿Se creó la cuenta?
  2. ¿Se generó el evento de verificación?
  3. ¿El proveedor aceptó el mensaje?
  4. ¿La bandeja de prueba lo recibió?
  5. ¿El test está leyendo el mensaje correcto?

Cuando todas se convierten en “espera hasta que aparezca un email”, cualquier retraso parece un fallo de aplicación. Ademas, si se reutiliza la misma bandeja entre tests paralelos, un mensaje viejo puede hacer que el caso equivocado pase.

Una solución sencilla es tratar el email como un evento observable. El endpoint no necesita esperar a que llegue el mensaje. Solo debe devolver una identidad de operación y registrar qué evento se produjo. La bandeja puede seguir siendo útil, pero ya no es la única fuente de verdad.

En un proyecto reciente, esta separación me resultó más clara junto con una guía para separar emails de signup por escenario. Cada escenario tiene su propio destino, y el evento lleva el contexto suficiente para no confundir registros.

Un contrato de eventos pequeño

No hace falta empezar con una plataforma de mensajería grande. Un contrato mínimo puede tener estos campos:

{
  "event_id": "evt_7f21",
  "event_type": "verification_email_requested",
  "account_id": "acc_204",
  "inbox_id": "test-inbox-17",
  "created_at": "2026-11-07T14:20:00Z",
  "status": "queued"
}
Enter fullscreen mode Exit fullscreen mode

La combinación de event_id e inbox_id es importante. El primero permite deduplicar. El segundo mantiene aislado el correo de un test respecto a los demás. Si usamos un generador de correo temporal para crear el destino, la aplicación debe guardar solo la referencia necesaria para ese caso, no copiar mensajes enteros a cada log.

Los estados pueden ser pocos: queued, sent, delivered y failed. No intentaria representar todas las situaciones del proveedor. El objetivo es que una prueba pueda saber si está esperando un evento que todavía no existe o una entrega que ya falló.

Un endpoint de webhook en FastAPI

El endpoint que recibe una confirmación del proveedor debe ser idempotente. Si llega dos veces el mismo evento, la segunda petición no debe crear una entrega nueva ni cambiar un estado final a otro más confuso.

from datetime import datetime
from typing import Literal

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class EmailEvent(BaseModel):
    event_id: str
    inbox_id: str
    status: Literal["sent", "delivered", "failed"]
    occurred_at: datetime


@app.post("/webhooks/email", status_code=204)
async def receive_email_event(event: EmailEvent) -> None:
    current = await load_event(event.event_id)
    if current is None:
        raise HTTPException(status_code=404, detail="event not found")

    if current.status in {"delivered", "failed"}:
        return

    await save_event_status(
        event_id=event.event_id,
        inbox_id=event.inbox_id,
        status=event.status,
        occurred_at=event.occurred_at,
    )
Enter fullscreen mode Exit fullscreen mode

En código real, load_event y save_event_status estarían detrás de una transacción o una operación condicional. La idea importante es que el evento tenga una clave estable y que la transición sea explícita. Si el proveedor reintenta el webhook, nuestra API responde igual sin duplicar trabajo.

Cómo probar sin carreras

Para un test de signup, prefiero este orden:

  1. Crear un inbox_id único para el caso.
  2. Crear la cuenta con ese identificador en el contexto de prueba.
  3. Esperar el evento verification_email_requested, no tres segundos arbitrarios.
  4. Consultar solo mensajes posteriores a created_at.
  5. Confirmar que el mensaje pertenece al event_id y al inbox_id.
  6. Consumir el enlace y guardar la evidencia mínima.

Si el sistema todavía no tiene eventos consultables, se puede envolver el polling en un helper que tenga un límite, una hora de inicio y una razón visible para cada reintento. Eso es mucho mejor que un while True con una pausa fija. Cuando el test falla, el log debe decir si no hubo evento, si hubo un evento sin mensaje o si el mensaje llegó pero no coincidía.

También conviene distinguir un timeout de infraestructura de un rechazo funcional. Un proveedor lento no significa que el token sea invalido. Esta distinción parece pequeña, pero reduce bastante el tiempo de diagnostico.

Checklist para el siguiente flujo

Antes de publicar otro endpoint de verificación, revisaria esto:

  • ¿Cada ejecución tiene un event_id y una bandeja propia?
  • ¿El endpoint puede recibir el mismo webhook dos veces?
  • ¿Los estados terminales se protegen contra cambios accidentales?
  • ¿El test espera una condición observable en vez de un tiempo fijo?
  • ¿Los logs omiten el contenido sensible del mensaje?
  • ¿Se conserva suficiente evidencia para explicar el fallo?

Un compañero puede escribir tempail en una búsqueda o en una nota de QA mientras intenta localizar una herramienta. No pasa nada, pero esa pista no debe convertirse en una regla de negocio ni en una dirección compartida por todos los tests.

La meta no es eliminar por completo el polling. En algunos entornos sigue siendo la integración disponible. La meta es darle un contexto: identidad, límite, evento esperado y motivo de salida. Con ese pequeño contrato, FastAPI queda más fácil de probar, los reintentos dejan mejor evidencia y el equipo puede corregir el punto real del flujo, no solo aumentar el sleep.

Top comments (0)