Aller à la documentation
Ruby

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.send_batch, templates.send et broadcasts.send), généré une fois par **appel** sous la forme oe- suivi d'un UUID aléatoire, 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 restent différentes.

Passez votre propre idempotency_key: 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. Un rejeu répond avec replayed à true et le message stocké dans son état actuel.

idempotency.rb
invoice_id = "inv_2026_09_4192" sent = client.emails.send(  from: "[email protected]",  to: "[email protected]",  subject: "Your September invoice",  text: "Invoice attached.",  idempotency_key: "invoice:#{invoice_id}") puts sent[:id], sent[:replayed]

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é avec un 422 idempotency_key_reuse au lieu d'être rejoué en silence. Une clé fait de 1 à 255 caractères parmi les lettres, les chiffres, _, ., : ou -, et tout le reste donne un 400 invalid_idempotency_key.

Tout le reste

Tout GET est réessayé. Une écriture ne l'est que lorsqu'une seconde requête identique ne peut rien signifier d'autre que la première, et un envoi entre dans ce cas parce que sa clé d'idempotence transforme une répétition en rejeu.

AppelRéessayéPourquoi
Tout GETOuiRien ne change.
emails.send, emails.send_batch, templates.send, broadcasts.sendOuiUne clé d'idempotence transforme une répétition en rejeu.
emails.cancel, emails.reschedule, broadcasts.cancel, forms.pause, forms.resume, threads.restoreOuiUne simple affectation d'un état nommé.
threads.update, threads.trashOuiUne affectation de libellés. L'appliquer deux fois revient à l'appliquer une fois.
threads.snooze, threads.unsnoozeOuiL'instant de réveil est dans le corps de la requête, il n'est pas déduit de l'heure d'arrivée.
emails.update, labels.update, webhooks.update, settings.update, roles.update, members.update, domains.update, domains.update_address, domains.update_address_forward, contacts.update, audiences.update, keys.update, forms.update, branding.update, threads.update_note, chats.rename, account.set_email_notification, account.set_push_muted, app_host.set, workspaces.set_activeOuiUne simple affectation de champs nommés.
members.grant_address, members.grant_domain, rules.reorder, threads.reorder_notesOuiUn octroi est un upsert, et un ordre est énoncé en entier.
templates.publish, forms.publish, imports.startOuiPublier ce qui est déjà publié, ou démarrer un import déjà démarré, renvoie l'élément inchangé.
templates.preview, templates.render, broadcasts.preview, rules.testOuiElles rendent, comptent ou évaluent, et n'écrivent rien.
domains.verify, app_host.verify, senders.researchOuiUne vérification répétée ne change rien d'autre que l'heure à laquelle elle a été faite.
contacts.save, contacts.set_audiences, contacts.remove_photo, contacts.block, contacts.unblock, contacts.delete_many, keys.revoke, files.revoke_link, files.revoke_all_links, app_host.delete, account.remove_photo, branding.remove_image, domains.remove_logo, domains.remove_logo_certificate, domains.remove_address_photo, account.accept_invitation, account.decline_invitation, forms.approve_submission, subscriptions.moveOuiChacun énonce le résultat final : un second appel laisse donc ce que le premier a laissé.
audiences.add_contact, audiences.add_contacts, audiences.remove_contacts, audiences.import_contacts, suppressions.add, domains.create_addressOuiUne répétition trouve le travail du premier appel déjà fait et le signale au lieu de le faire deux fois.
contacts.set_photo, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate, domains.set_address_photo, imports.upload_chunkOuiLes octets renvoyés remplacent ce que la première tentative a stocké.
drafts.create, labels.create, webhooks.create, templates.create, rules.create, roles.create, temp_mail.create, files.uploadNonUn réessai laisse deux objets.
drafts.updateNonLisez 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.revoke_address, temp_mail.delete, temp_mail.delete_messageNonUn réessai après une réponse perdue signale un échec pour un travail qui a réussi.
webhooks.rotate_secretNonUne seconde rotation invalide le secret que la première tentative a renvoyé.
webhooks.testNonCela enverrait une seconde livraison synthétique.
webhooks.replay_deliveryNonIl enverrait l'événement une seconde fois à votre destinataire.
emails.translate, emails.compose, emails.rewrite, emails.suggest_subjectNonChacune 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.
security.begin_step_up, security.verify_step_upNonUn réessai pourrait envoyer un deuxième e-mail ou consommer un deuxième essai sur le code.
Tout autre appel qui n'est pas un GETNonEnvoyée une seule fois ; un échec est signalé plutôt que répété.

Le backoff

  • Borné par max_retries: sur le client, avec deux tentatives supplémentaires par défaut. max_retries: 0 désactive les réessais.
  • 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 gigue : chaque attente est un point aléatoire entre la moitié de ce plafond et sa totalité, pour qu'une flotte ne se resynchronise pas au moment du rétablissement.
  • 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 retry_after_seconds dessus. Revenir plus tôt que demandé, ce n'est pas respecter la consigne.
  • Un dépassement de délai est un échec réseau comme un autre : un appel qui peut être répété sans risque est donc retenté après un tel échec, et timeout: s'applique de nouveau à chaque tentative.
  • Les attentes sont des appels à sleep à l'intérieur de l'appel : le thread appelant attend donc aussi, et l'appel ne retourne ou ne lève une erreur qu'une fois sa dernière tentative terminée.