Перейти к документации
SDK

Повторы и идемпотентность

Что повторяется, что намеренно не повторяется и почему повторённая отправка не может продублировать письмо.

Отправки

Клиент добавляет Idempotency-Key к каждой отправке (emails.send, emails.sendBatch и templates.send); ключ генерируется один раз на **вызов** и переиспользуется повторами этого вызова. API занимает этот ключ до того, как что-либо отправит, поэтому повтор воспроизводит исходное письмо, а не отправляет второе, тогда как два намеренных вызова send() по-прежнему отправляют дважды. Это разные намерения, и они остаются разными.

Передайте собственный idempotencyKey, чтобы растянуть эту гарантию на несколько процессов: задача, которая упала и запустилась снова, воспроизведёт свои отправки, а не повторит их.

idempotency.ts
await openemail.emails.send(message, { idempotencyKey: `invoice:${invoice.id}` })

Выводите его из того, что сделало отправку необходимой. Никогда — из часов. Переиспользование ключа с другим телом отклоняется с idempotency_key_reuse, а не воспроизводится молча.

Всё остальное

Любое чтение повторяется. Запись повторяется только там, где второй идентичный запрос не может означать ничего иного, чем первый, а отправка подходит под это, потому что её ключ идемпотентности превращает повтор в воспроизведение.

ВызовПовторяетсяПочему
Любое чтениеДаНичего не меняется.
`emails.send`, `emails.sendBatch`, `templates.send`ДаКлюч идемпотентности превращает повтор в воспроизведение.
`emails.cancel`, `emails.reschedule`ДаЧистая установка названного состояния.
`threads.update`, `threads.trash`ДаУстановка набора ярлыков. Применить её дважды — то же самое, что применить один раз.
`threads.snooze`, `threads.unsnooze`ДаМомент пробуждения задан в теле, а не выводится из времени поступления запроса.
`labels.update`, `webhooks.update`, `settings.update`, `roles.update`, `members.update`ДаЧистая установка названных полей.
`members.grantAddress`, `rules.reorder`ДаВыдача выполняется как upsert, а порядок задан полностью.
`templates.publish`ДаПубликация вершины, которая уже опубликована, возвращает её без изменений.
`templates.preview`, `rules.test`ДаОни выполняют отрисовку или вычисление и ничего не записывают.
`drafts.create`, `labels.create`, `webhooks.create`, `templates.create`, `rules.create`, `roles.create`, `tempMail.create`НетПовтор оставит два объекта.
`drafts.update`НетЧитайте идентификатор из результата каждой записи, а не переиспользуйте тот, что отправили.
`drafts.delete`, `labels.delete`, `webhooks.delete`, `templates.delete`, `rules.delete`, `roles.delete`, `members.remove`, `members.revokeAddress`, `tempMail.delete`, `tempMail.deleteMessage`НетПовтор после потерянного ответа сообщит об ошибке для работы, которая удалась.
`webhooks.rotateSecret`НетВторая ротация обесценивает секрет, который вернула первая попытка.
`webhooks.test`НетОн отправил бы вторую синтетическую доставку.
`emails.translate`НетОн тратит вызовы модели, поэтому повтор после оставшегося без ответа запроса оплачивает один и тот же ответ дважды.
Любая другая записьНетОтправляется один раз, и о сбое сообщается, а не повторяется запрос.

Схема отката

  • Ограничено параметром maxRetries на клиенте; по умолчанию — две дополнительные попытки.
  • Только после сетевого сбоя или 408, 500, 502, 503 либо 504. 429 повторяется, только если он несёт Retry-After, а этот API его не присылает, поэтому превышение лимита сразу выбрасывает исключение. Любой другой статус выбрасывает исключение немедленно.
  • Экспоненциально от половины секунды до восьми, с разбросом, чтобы парк машин не синхронизировался заново на восстановлении.
  • Темп задаётся заголовком Retry-After в любой из его форм — delay-seconds и HTTP-date. Когда сервер называет время ожидания, клиент ждёт ровно столько, а не отступает по своей схеме.
  • Сервер, просящий подождать дольше минуты, трактуется как велящий клиенту остановиться, а не поспать, поэтому ошибка выбрасывается с полем retryAfterSeconds. Вернуться раньше, чем он просил, — значит не выполнить его просьбу.
  • AbortSignal вызывающего никогда не приводит к повтору. Прерывание немедленно выбрасывает OpenEmailNetworkError — из запроса или из ожидания перед следующей попыткой.