Прежде чем повторять POST после сбоя прокси, выясните, что целевой сервер, возможно, уже сделал. Потеря ответа оставляет исход операции неопределённым. Второй запрос может создать ещё одну задачу, хотя первая уже существует.
В нашей локальной демонстрации клиент получил ошибку после того, как origin создал задачу. Повтор POST создал вторую задачу. Повтор с тем же ключом тоже создал вторую задачу, когда принимающая конечная точка ключ игнорировала. Только реализованный на стороне конечной точки контракт дедупликации заставил повтор с тем же ключом вернуть исходную задачу.
Это синтетические записи о задачах в памяти, а не выполненные боевые задания. Полезный результат — разница между числом предпринятых запросов и числом наблюдаемых эффектов в приложении. Оба можно воспроизвести из загружаемого архива.
Потеряйте ответ после создания задачи
Скачайте и распакуйте демонстрацию повторов через прокси. Из распакованного каталога выполните:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.txt
python retry_jobs_demo.py > result.json
python retry_jobs_demo.py --csv result.jsonПример требует Python 3.11 или новее и фиксирует HTTPX 0.28.1 с HTTPcore 1.0.9. Он использует asyncio.timeout, добавленный в Python 3.11. Установка зависимостей обращается к индексу пакетов. Сама демонстрация запускает временный HTTP-origin и forward-прокси на loopback; ни аккаунт провайдера, ни внешний целевой сервер не задействованы.
В каждом из шести отдельных случаев origin применяет первый POST и отправляет ответ. Прокси читает этот полный ответ, а затем намеренно закрывает соединение в сторону клиента, не отправив заголовки ответа. В этом зафиксированном прогоне клиент записывает RemoteProtocolError. Любой повторный запрос или запрос состояния — это последующее явное действие клиента.
Зафиксированный порядок событий:
origin: job applied
origin: response sent
proxy: complete origin response received
proxy: response dropped before client headers
client: request failedЭту матрицу дали собственные записи origin. Число POST включает исходную попытку; запрос состояния — это отдельный GET.
| Действие после потерянного ответа | Получено POST | Получено GET | Создано задач | Следующий результат клиента |
|---|---|---|---|---|
| Повторить POST без контракта повтора | 2 | 0 | 2 | 201, вторая задача |
| Переиспользовать ключ, который конечная точка игнорирует | 2 | 0 | 2 | 201, вторая задача |
| Переиспользовать поддерживаемый ключ с неизменным входом | 2 | 0 | 1 | 201, исходный сохранённый результат |
| Сгенерировать новый ключ для повтора | 2 | 0 | 2 | 201, вторая задача |
| Переиспользовать поддерживаемый ключ с изменённым входом | 2 | 0 | 1 | 409, конфликт входных данных |
| Запросить состояние подтверждённой операции; второй POST не отправлять | 1 | 1 | 1 | 200, существующая задача найдена |
Таким образом, 201 на второй попытке означал в этой фикстуре две разные вещи: новую вторую задачу или сохранённый результат первой. Чтобы их различить, мы сверяли идентификаторы задач и счётчики эффектов на origin. Идемпотентность касается ожидаемого эффекта повторения операции; один лишь код статуса её не доказывает. Семантика идемпотентности в HTTP.
Важная граница проходит между применением операции и доставкой её ответа. Это не то же самое, что попытка соединения, провалившаяся до отправки запроса приложения. Для HTTPS-сервера сбой при установлении туннеля CONNECT тоже происходит на другой стадии, нежели потеря ответа на POST, отправленный через уже установленный туннель. Эти стадии разобраны в руководстве по таймаутам.
Прокси в этой демонстрации запросы не повторяет. Второй POST — явное действие клиента. Это различие существенно: семантика HTTP запрещает прокси автоматически повторять неидемпотентный запрос. Клиенту тоже нужно основание считать неидемпотентную операцию повторяемой либо свидетельство того, что исходная не была применена. RFC 9110, раздел 9.2.2.
Выбирайте следующее действие по состоянию операции
Класс исключения описывает наблюдение клиента. Это не квитанция от приложения. Чтобы решить, что делать дальше, используйте свидетельства о логической операции — то есть о задаче, которую вызывающая сторона намеревалась создать.
| Доступные свидетельства | Что вы знаете | Следующее действие |
|---|---|---|
| Авторитетный результат идентифицирует созданную задачу | Создание вступило в силу | Используйте или сверьте этот результат; не создавайте задачу заново |
| Исход неизвестен, но конечная точка поддерживает повтор с вашим сохранённым ключом операции и неизменным входом | Её документированный контракт может позволить повторить попытку без повторения эффекта | Переиспользуйте этот идентификатор в рамках области действия и правил хранения конечной точки |
| Надёжные свидетельства устанавливают, что ни одна попытка не применилась и уже не может примениться | Логическая операция не вступила в силу | Новую попытку можно рассматривать в пределах обычного бюджета запросов |
| Исход неизвестен, и применимого контракта повтора нет | Повтор POST может создать второй эффект | Используйте процесс статуса/сверки сервиса или эскалируйте нерешённую операцию |
В случае с запросом состояния вызывающая сторона сохранила X-Operation-Ref: operation-1; GET /operations/operation-1 фикстуры возвращает связанную задачу. Эта ссылка была известна до POST, поэтому для запроса состояния не нужен ID задачи из потерянного ответа.
С пустым или неудачным запросом состояния обращайтесь осторожно. «Не найдено» в один момент времени не означает, что более ранний запрос не может прийти или завершиться позже. Положительный запрос состояния в демонстрации работает потому, что возвращает уже созданную задачу. Безопасность отрицательного ответа он не проверяет. Это различие следует из проблемы запоздалого прибытия, разобранной в AWS Builders Library.
Задержка с backoff меняет момент следующей попытки. Она не устанавливает, произошла ли первая операция. Ответу 502, 504, таймауту или разорванному соединению по-прежнему нужны фаза запроса и контекст приложения. Используйте статью о кодах статуса прокси, чтобы определить отвечающий уровень; не превращайте её категории ошибок в универсальный список безопасных повторов POST.
Заголовку нужно принимающее приложение, которое его понимает
Фикстура с поддерживаемым ключом записывает задачу и её ответ под блокировкой внутри процесса. В рамках её области с одним вызывающим ключ привязан к конечной точке POST и сравнивается со ссылкой на операцию и разобранной полезной нагрузкой. Совпадающий повтор получает сохранённый результат задачи. Изменённый вход получает явный ответ 409 фикстуры без создания ещё одной задачи.
Случай с игнорируемым ключом меняет поведение принимающего приложения, а не написание заголовка. Случай с новым ключом меняет идентификатор операции, предъявляемый приложению. Оба создали две задачи. Переиспользуйте идентификатор задуманной операции, когда её контракт допускает повтор; генерация нового ключа на каждую попытку эту связь разрушает.
Эта фикстура хранит записи только в памяти на время своей жизни. Её блокировка демонстрирует координацию внутри одного процесса. Она не делает создание задач долговечным, не покрывает перезапуск, не истекает ключи безопасно и не координирует внешние эффекты вроде отправки сообщения. Для этих свойств нужен дизайн приложения за рамками данного примера.
Реальные API определяют собственные контракты. Например, Stripe документирует сохранение результата запроса для переиспользования с тем же ключом, проверку последующих параметров и трактовку повторно использованного ключа как нового запроса после удаления сохранённой записи. Это правила Stripe; отправка похоже названного заголовка другой конечной точке их не импортирует. Идемпотентные запросы Stripe.
Прежде чем полагаться на ключ в продакшене, проверьте четыре вещи у принимающего сервиса:
- Идентичность и область действия: какого вызывающего, аккаунт, конечную точку и операцию идентифицирует ключ?
- Вход и конкурентность: как обрабатываются изменённые входные данные и перекрывающиеся попытки?
- Хранение и восстановление: как долго действует защита и что переживает перезапуск или частичный сбой?
- Семантика результата: что возвращается для существующей, ожидающей, неудавшейся или завершённой операции?
Обсуждение дизайна у AWS связывает идентичность запроса с атомарной записью мутации и объясняет, почему запоздалые прибытия и изменённое намерение требуют явной обработки. Это полезный фон для этих вопросов, а не свидетельство того, что каждый API реализует такие же гарантии. Making retries safe with idempotent APIs.
Разделяйте клиентские повторы и бизнес-результаты
Не используйте число успешных ответов у клиента как число задач. Записывайте идентификатор логической операции отдельно от каждой транспортной попытки. Когда ответ потерян, сохраняйте исход как нерешённый, пока контракт сервиса или авторитетное состояние не позволят сделать более сильный вывод. Так в вашем расследовании восстановленный ответ, воспроизведённый результат и вновь созданная задача останутся различимыми.
Будьте точны и в настройках библиотек. HTTPX документирует свои встроенные транспортные повторы для ошибок соединения и таймаутов соединения, а не для произвольных сбоев чтения/записи или кодов статуса. В зафиксированной реализации HTTPX 0.28.1 ветка HTTP-прокси не передаёт настройку retries транспорта в свой пул прокси. Этот эксперимент использует явные действия клиента и ноль повторов по умолчанию; он не зависит от того, что эта опция воспроизведёт проксированный POST. Документация HTTPX по транспортам, исходный код транспорта на теге.
Для мониторинга считайте попытки, наблюдаемые сбои ответов, подтверждённые задачи и нерешённые логические операции по отдельности. Статья о методологии бенчмарков объясняет, почему важен знаменатель в виде числа попыток. Для интеграции, меняющей состояние, добавьте результат в приложении, прежде чем называть восстановление успешным.
Загрузки и границы теста
Архив содержит полную демонстрацию, тесты, зафиксированные версии, README, записанный JSON и производный от него CSV. По отдельности можно посмотреть код, тесты, зависимости, README, вывод событий и матрицу результатов.
ipvolt выполнил локальные проверки 13 сентября 2026 года на CPython 3.14.7 под macOS arm64 с указанными выше зафиксированными пакетами. Проверки покрывают матрицу из шести случаев, перекрывающиеся запросы с одним ключом, конфликты изменённого входа, различные идентификаторы, отказ пересылать на внешний целевой сервер и очистку фикстуры. Записанные случаи завершились с закрытым клиентом и без оставшихся обработчиков или писателей фикстуры.
Тест использует пересылку по HTTP/1.1. Он не проверяет CONNECT, TLS, аутентификацию, SOCKS, внешних провайдеров, долговечное хранилище, истечение ключей или поведение удалённого сервиса при запоздалом прибытии. Он не делает заявлений о надёжности провайдера или об обработке exactly-once.
Если хотите узнать, когда откроется доступ к ipvolt, запишитесь в список раннего доступа. Одно письмо, когда откроется доступ. Больше ничего. Эта демонстрация не описывает доступный API ipvolt или функцию идемпотентности ipvolt.