Aller à la documentation
SDK

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.

idempotency.ts
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.

AppelRéessayéPourquoi
Toute lectureOuiRien ne change.
`emails.send`, `emails.sendBatch`, `templates.send`OuiUne clé d'idempotence transforme une répétition en rejeu.
`emails.cancel`, `emails.reschedule`OuiUne simple affectation d'un état nommé.
`threads.update`, `threads.trash`OuiUne affectation de libellés. L'appliquer deux fois revient à l'appliquer une fois.
`threads.snooze`, `threads.unsnooze`OuiL'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`OuiUne simple affectation de champs nommés.
`members.grantAddress`, `rules.reorder`OuiL'octroi est un upsert, et l'ordre est énoncé en entier.
`templates.publish`OuiPublier une tête déjà publiée la renvoie inchangée.
`templates.preview`, `rules.test`OuiElles rendent ou évaluent, et n'écrivent rien.
`drafts.create`, `labels.create`, `webhooks.create`, `templates.create`, `rules.create`, `roles.create`, `tempMail.create`NonUn réessai laisse deux objets.
`drafts.update`NonLisez 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`NonUn réessai après une réponse perdue signale un échec pour un travail qui a réussi.
`webhooks.rotateSecret`NonUne seconde rotation invalide le secret que la première tentative a renvoyé.
`webhooks.test`NonCela enverrait une seconde livraison synthétique.
`emails.translate`NonCela 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 écritureNonEnvoyée une seule fois ; un échec est signalé plutôt que répété.

Le backoff

  • Borné par maxRetries sur le client, avec deux tentatives supplémentaires par défaut.
  • Uniquement après un échec réseau ou un 408, 500, 502, 503 ou 504. Un 429 n'est réessayé que s'il porte un Retry-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-After dans 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 retryAfterSeconds dessus. Revenir plus tôt que demandé, ce n'est pas respecter la consigne.
  • Un AbortSignal fourni par l'appelant n'est jamais réessayé. Une interruption lève immédiatement une OpenEmailNetworkError, depuis la requête ou depuis l'attente qui précède la tentative suivante.