DEV Community

Silviu Technology
Silviu Technology

Posted on

FastAPI: reintentos seguros para emails de prueba

Los flujos de email de prueba suelen fallar por una razón poco visible: un timeout no significa que la operación no ocurrió. Un cliente puede repetir la llamada, un worker puede procesar el mismo evento dos veces y un test puede hacer polling mientras el estado todavía cambia.

En proyectos Python suelo tratar este problema como un contrato de API, no como un simple try/except. La idea es que cada reintento tenga una respuesta predecible, que el backend no duplique el trabajo y que los logs permitan entender lo que pasó. Este patrón funciona especialmente bien con FastAPI y es fácil de adaptar a automatización de CI.

El problema: un reintento no es una nueva operación

Imagina un endpoint que crea un buzón de prueba y espera un mensaje:

POST /test-mailboxes
{
  "purpose": "signup"
}
Enter fullscreen mode Exit fullscreen mode

Si la respuesta tarda más que el timeout del cliente, repetir el POST puede crear dos recursos. El test quizá solo observa uno y deja el segundo consumiendo espacio. En una ejecución puntual parece un fallo de red, pero despues se convierte en datos huérfanos y resultados difíciles de comparar.

La solución empieza con una distinción sencilla: el request_id identifica la petición, mientras que la Idempotency-Key identifica la operación que se puede repetir. La segunda debe persistirse durante el tiempo suficiente para que el cliente reciba el resultado original.

Define estados antes de escribir el endpoint

Para un flujo de email de prueba, unos estados pequeños son más útiles que una cadena de mensajes libres:

created -> waiting_for_message -> received
                         \-> expired
                         \-> failed
Enter fullscreen mode Exit fullscreen mode

Cada transición debería guardar updated_at, un motivo corto y un identificador de correlación. No guardes el mensaje completo en el log: con el asunto, el message_id hash y el tamaño suele bastar para investigar.

También conviene separar el estado de la solicitud del estado del mensaje. Una solicitud puede estar completed aunque el mensaje recibido sea un rebote, por ejemplo. Esa separación evita que un reintento cambie silenciosamente el significado del resultado.

Si el producto necesita generar una dirección para pruebas, una búsqueda como temp mail generator puede llevar a opciones distintas; en el código importa más definir la duración, el aislamiento y la limpieza del recurso. Para pruebas manuales, obtener un correo temporal puede ser útil, pero no lo uses para datos personales ni como dependencia oculta de una suite automatizada.

Una clave de idempotencia en FastAPI

El handler puede recibir la clave desde una cabecera y delegar la decisión a una capa de servicio. El ejemplo es pequeño a propósito:

from fastapi import FastAPI, Header, HTTPException

app = FastAPI()

@app.post("/test-mailboxes")
async def create_mailbox(
    purpose: str,
    idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
):
    if not idempotency_key:
        raise HTTPException(status_code=400, detail="Falta Idempotency-Key")

    cached = await store.find_request(idempotency_key)
    if cached:
        return cached.response

    result = await mailbox_service.create(purpose=purpose)
    await store.save_request(idempotency_key, result.response)
    return result.response
Enter fullscreen mode Exit fullscreen mode

En producción, find_request y save_request deben participar en una garantía real de unicidad. Un diccionario en memoria sirve para una demo, pero no para varios workers. Usa una restricción única en PostgreSQL o Redis con una política de expiración y considera qué ocurre si el proceso muere después de crear el buzón pero antes de guardar la respuesta.

La clave también debe estar ligada a una huella de la petición. Si el mismo cliente reutiliza la clave con otro purpose, responde 409 Conflict. Aceptarlo como si nada produce bugs muy raros de reproducir.

Polling y webhooks con evidencia útil

El polling necesita límites claros: intervalo, deadline y estado terminal. Un test que pregunta indefinidamente oculta una regresión. Una implementación simple puede usar backoff y detenerse cuando received, expired o failed aparece.

Para el equipo frontend, es útil validar emails sin respuestas viejas y mostrar el correlation_id cuando algo termina. Si el trabajo tarda, una cola con estados visibles también ayuda a hacer visibles las colas de emails largos.

Un webhook debe ser idempotente por event_id. Antes de ejecutar una acción, guarda ese identificador con una restricción única. Si llega de nuevo, devuelve 200 y no repitas el efecto. La respuesta exitosa significa “evento conocido y procesado”, no necesariamente “se hizo trabajo nuevo”. Es un matiz pequeño, pero ahorra alertas falsas.

Preguntas rápidas sobre reintentos

¿Cuánto tiempo guardo una clave?

Durante la ventana máxima de reintento del cliente, más un margen. Para una suite de CI, una hora puede ser suficiente; elige según tu flujo y documentalo. No existe un número universal.

¿Un timeout debe devolver error siempre?

No necesariamente. El cliente puede recibir 202 Accepted con un estado consultable. Si ya existe una respuesta final asociada a la clave, devuelve ese mismo resultado para que el consumidor no tenga que adivinar.

¿Cómo pruebo que la idempotencia funciona?

Envía dos peticiones concurrentes con la misma clave y comprueba que solo exista un recurso. Después repite con el mismo valor y un cuerpo diferente: debe fallar de forma explícita.

Checklist final

  • [ ] Hay una clave de idempotencia para cada operación repetible.
  • [ ] La base de datos impone unicidad, incluso con varios workers.
  • [ ] Los estados terminales y sus motivos están documentados.
  • [ ] Polling tiene deadline y no confunde un mensaje viejo con uno nuevo.
  • [ ] Webhooks deduplican por event_id.
  • [ ] Los logs no contienen tokens, direcciones privadas ni cuerpos completos.
  • [ ] La limpieza de buzones de prueba tiene un TTL verificable.

Este diseño no elimina todos los fallos de red. Hace algo más valioso: convierte los reintentos en comportamiento definido. Con estados pequeños, claves persistentes y evidencia suficiente, FastAPI puede mantener los tests rápidos sin volverlos frágiles.

En documentación y búsquedas también aparecerán expresiones como temp org mail o dummy e mail; tratarlas como entradas imperfectas, no como estados válidos del dominio, ayuda a que el sistema sea más tolerante sin contaminar sus datos internos.

Top comments (0)