Réessais et idempotence
Ce qui est réessayé, ce qui ne l'est délibérément pas, et pourquoi un envoi réessayé ne peut pas faire de doublon.
Envois
Le client attache un Idempotency-Key à chaque envoi (emails.send, emails.sendBatch et templates.send), généré une fois par **appel** et réutilisé par les réessais de cet appel. L'API réserve cette clé avant de dispatcher quoi que ce soit : un réessai rejoue donc le message d'origine au lieu d'en envoyer un second, tandis que deux appels délibérés à send() envoient bien deux fois. Ce sont des intentions différentes, et elles le restent.
Passez votre propre idempotencyKey pour étendre cette garantie au-delà d'un même processus : un job qui a planté puis s'est relancé rejoue ses envois au lieu de les répéter.
await openemail.emails.send(message, { idempotencyKey: `invoice:${invoice.id}` })Dérivez-la de ce qui a rendu l'envoi nécessaire. Jamais d'une horloge. Réutiliser une clé avec un corps différent est refusé par idempotency_key_reuse plutôt que rejoué en silence.
Tout le reste
Toute lecture est réessayée. Une écriture ne l'est que lorsqu'une seconde requête identique ne peut rien signifier d'autre que la première ; un envoi entre dans ce cas parce que sa clé d'idempotence transforme une répétition en rejeu.
| Appel | Réessayé | Pourquoi |
|---|---|---|
| Toute lecture | Oui | Rien ne change. |
| `emails.send`, `emails.sendBatch`, `templates.send` | Oui | Une clé d'idempotence transforme une répétition en rejeu. |
| `emails.cancel`, `emails.reschedule` | Oui | Une simple affectation d'un état nommé. |
| `threads.update`, `threads.trash` | Oui | Une affectation de libellés. L'appliquer deux fois revient à l'appliquer une fois. |
| `threads.snooze`, `threads.unsnooze` | Oui | L'instant de réveil est dans le corps de la requête, il n'est pas déduit de l'heure d'arrivée. |
| `labels.update`, `webhooks.update`, `settings.update`, `roles.update`, `members.update` | Oui | Une simple affectation de champs nommés. |
| `members.grantAddress`, `rules.reorder` | Oui | L'octroi est un upsert, et l'ordre est énoncé en entier. |
| `templates.publish` | Oui | Publier une tête déjà publiée la renvoie inchangée. |
| `templates.preview`, `rules.test` | Oui | Elles rendent ou évaluent, et n'écrivent rien. |
| `drafts.create`, `labels.create`, `webhooks.create`, `templates.create`, `rules.create`, `roles.create`, `tempMail.create` | Non | Un réessai laisse deux objets. |
| `drafts.update` | Non | Lisez l'id sur le résultat de chaque écriture plutôt que de réutiliser celui que vous avez envoyé. |
| `drafts.delete`, `labels.delete`, `webhooks.delete`, `templates.delete`, `rules.delete`, `roles.delete`, `members.remove`, `members.revokeAddress`, `tempMail.delete`, `tempMail.deleteMessage` | Non | Un réessai après une réponse perdue signale un échec pour un travail qui a réussi. |
| `webhooks.rotateSecret` | Non | Une seconde rotation invalide le secret que la première tentative a renvoyé. |
| `webhooks.test` | Non | Cela enverrait une seconde livraison synthétique. |
| `emails.translate` | Non | Cela consomme des appels au modèle : un réessai après une requête restée sans réponse paie deux fois la même réponse. |
| Toute autre écriture | Non | Envoyée une seule fois ; un échec est signalé plutôt que répété. |
Le backoff
- Borné par
maxRetriessur le client, avec deux tentatives supplémentaires par défaut. - Uniquement après un échec réseau ou un
408,500,502,503ou504. Un429n'est réessayé que s'il porte unRetry-After, et cette API n'en envoie pas : une limite de débit lève donc une erreur immédiatement. Tout autre statut lève une erreur sur-le-champ. - Exponentiel, d'une demi-seconde jusqu'à huit, avec du jitter, pour qu'un parc de machines ne se resynchronise pas au moment de la reprise.
- Cadencé par
Retry-Afterdans l'une ou l'autre de ses formes, delay-seconds et HTTP-date. Quand le serveur indique une attente, le client patiente exactement ce temps-là au lieu d'appliquer son backoff. - Un serveur qui demande plus d'une minute est compris comme disant au client de s'arrêter plutôt que d'attendre : l'erreur est donc levée avec
retryAfterSecondsdessus. Revenir plus tôt que demandé, ce n'est pas respecter la consigne. - Un
AbortSignalfourni par l'appelant n'est jamais réessayé. Une interruption lève immédiatement uneOpenEmailNetworkError, depuis la requête ou depuis l'attente qui précède la tentative suivante.