Reintentos e idempotencia
Qué se reintenta, qué no se reintenta a propósito y por qué un envío reintentado no puede duplicarse.
Envíos
El cliente adjunta un Idempotency-Key a cada envío (emails.send, emails.sendBatch y templates.send), generado una vez por **llamada** y reutilizado por los reintentos de esa llamada. La API reserva esa clave antes de despachar nada, así que un reintento reproduce el mensaje original en lugar de enviar uno segundo, mientras que dos llamadas deliberadas a send() siguen enviando dos veces. Son intenciones distintas y siguen siéndolo.
Pasa tu propio idempotencyKey para extender esa garantía entre procesos, de modo que un trabajo que se cayó y volvió a ejecutarse reproduzca sus envíos en lugar de repetirlos.
await openemail.emails.send(message, { idempotencyKey: `invoice:${invoice.id}` })Dedúcelo de aquello que hizo necesario el envío. Nunca de un reloj. Reutilizar una clave con un cuerpo distinto se rechaza con idempotency_key_reuse en lugar de reproducirse en silencio.
Todo lo demás
Toda lectura se reintenta. Una escritura solo se reintenta cuando una segunda petición idéntica no puede significar nada distinto de la primera, y un envío cumple esa condición porque su clave de idempotencia convierte una repetición en una reproducción.
| Llamada | Se reintenta | Por qué |
|---|---|---|
| Toda lectura | Sí | No cambia nada. |
| `emails.send`, `emails.sendBatch`, `templates.send` | Sí | Una clave de idempotencia convierte una repetición en una reproducción. |
| `emails.cancel`, `emails.reschedule` | Sí | Una asignación pura de un estado con nombre. |
| `threads.update`, `threads.trash` | Sí | Una asignación de etiquetas. Aplicarla dos veces equivale a aplicarla una vez. |
| `threads.snooze`, `threads.unsnooze` | Sí | El instante de reactivación viaja en el cuerpo; no se deriva de la hora de llegada. |
| `labels.update`, `webhooks.update`, `settings.update`, `roles.update`, `members.update` | Sí | Una asignación pura de campos con nombre. |
| `members.grantAddress`, `rules.reorder` | Sí | La concesión es un upsert, y el orden se indica por completo. |
| `templates.publish` | Sí | Publicar un head que ya está publicado lo devuelve sin cambios. |
| `templates.preview`, `rules.test` | Sí | Renderizan o evalúan, y no escriben nada. |
| `drafts.create`, `labels.create`, `webhooks.create`, `templates.create`, `rules.create`, `roles.create`, `tempMail.create` | No | Un reintento deja dos objetos. |
| `drafts.update` | No | Lee el id del resultado de cada escritura en lugar de reutilizar el que enviaste. |
| `drafts.delete`, `labels.delete`, `webhooks.delete`, `templates.delete`, `rules.delete`, `roles.delete`, `members.remove`, `members.revokeAddress`, `tempMail.delete`, `tempMail.deleteMessage` | No | Un reintento tras una respuesta perdida informa de un fallo en un trabajo que sí se completó. |
| `webhooks.rotateSecret` | No | Una segunda rotación invalida el secreto que devolvió el primer intento. |
| `webhooks.test` | No | Enviaría una segunda entrega sintética. |
| `emails.translate` | No | Consume llamadas al modelo, así que un reintento tras una solicitud sin respuesta paga dos veces la misma respuesta. |
| Cualquier otra escritura | No | Se envía una sola vez, y un fallo se informa en lugar de repetirse. |
El backoff
- Limitado por
maxRetriesen el cliente, con dos intentos adicionales por defecto. - Solo tras un fallo de red o un
408,500,502,503o504. Un429se reintenta únicamente cuando lleva unRetry-After, y esta API no lo envía, así que un límite de tasa lanza un error de inmediato. Cualquier otro estado lanza al instante. - Exponencial, desde medio segundo hasta ocho, con jitter, para que una flota no se resincronice al recuperarse el servicio.
- Regulado por
Retry-Afteren cualquiera de sus formas, delay-seconds y HTTP-date. Cuando el servidor indica una espera, el cliente espera exactamente ese tiempo en lugar de aplicar backoff. - Si el servidor pide más de un minuto, se interpreta como una orden de detenerse y no de esperar, así que el error se lanza con
retryAfterSecondsincluido. Volver antes de lo que pidió no es respetarlo. - El
AbortSignalde quien llama nunca se reintenta. Abortar lanza unOpenEmailNetworkErrorde inmediato, ya sea desde la solicitud o desde la espera previa al siguiente intento.