Перейти к содержанию

Ключ повторяемой операции

Повтор POST способен создать второй курс, если первая попытка завершилась, но её ответ потерялся. Простое сравнение одинаковых названий не решает задачу: похожие записи иногда создаются намеренно. Клиенту нужен способ обозначить одно конкретное намерение, а серверу — сохранить его результат и распознать повтор.

В этом уроке вводим Idempotency-Key как прикладной договор нашего учебного API. Не объявляем его завершённым универсальным стандартом для всех сервисов. Результат — правила создания курса, которые связывают ключ с редактором, операцией и содержимым. Реальная база и запросы не запускаются; состояния ниже описывают требуемое поведение будущей реализации.

Один ключ на одно намерение

Пусть редактор нажал «Создать курс» и клиент сформировал новый непредсказуемый ключ. Значение в листинге условное; для реального клиента потребуется генератор с достаточной уникальностью. Повтор того же намерения использует прежний ключ, а новое сознательное создание получает новый.

POST /api/courses HTTP/1.1
Host: catalog.example
Content-Type: application/json
Accept: application/json, application/problem+json
Idempotency-Key: create-course-a7b91

{"title":"HTTP и API","topic":"backend","lessons":32}

Считаем, что личность и право редактора установлены вне данного листинга. Область ключа в нашем сервисе состоит из субъекта редактора, POST к коллекции курсов и значения ключа. Поэтому одинаковая строка другого редактора не позволяет ему получить чужой сохранённый результат. Авторизация проверяется независимо от наличия знакомого ключа.

Ключ не является идентификатором нового курса. Сервер по-прежнему создаёт c005 и возвращает её адрес. Клиент хранит ключ рядом с состоянием отправки формы, пока намерение имеет неопределённый исход. Перезагрузка страницы не должна автоматически превратить повтор старой отправки в новый ключ, иначе защита исчезнет именно при восстановлении после сбоя.

Сохранённый результат

После первого принятия операции сервер возвращает обычный результат создания:

HTTP/1.1 201 Created
Location: /api/courses/c005
Content-Type: application/json
Cache-Control: no-store

{"id":"c005","title":"HTTP и API","topic":"backend","lessons":32}

Если клиент не получил это сообщение и повторил тот же запрос с тем же ключом, сервер отдаёт сохранённые статус, Location и тело. Вторая попытка не создаёт c006. Это правило нашего сервиса о воспроизведении результата; оно точнее, чем обещание, что «POST теперь всегда идемпотентен» независимо от ключа и срока.

Результат включает то, что клиенту нужно для завершения намерения. Технические поля передачи будущая библиотека может сформировать заново; не требуется сохранять сырые байты TCP-соединения. Важны согласованные статус и предметное представление. Поэтому потерянный ответ восстанавливается по записи операции, а не через случайный поиск похожего title в каталоге.

Сохранение курса и записи успешной операции должно быть атомарным. Если база сохранила c005, а ключ потеряла, повтор создаст дубликат. Если ключ сохранён как успех, но курс не появился, клиент получит ложное подтверждение. Эти два изменения входят в одну границу целостности будущей реализации. Прикладное проектирование повторов.

Что считается тем же входом

Вместе с ключом сохраняется отпечаток согласованного запроса. Наш договор сравнивает разобранные и нормализованные редактируемые поля: title после удаления крайних пробелов, topic и целое lessons. Порядок ключей JSON и незначимые пробелы записи не делают намерение другим, поскольку предметный вход одинаков.

При этом изменение lessons с 32 на 33 под тем же ключом не должно молча возвращать старое c005. Клиент отправил другие данные и повторно использовал идентификатор прежнего намерения. Сервис отвечает 409 с типом idempotency-conflict; новый курс не создаётся и прежний не исправляется этим POST.

{"type":"https://catalog.example/problems/idempotency-conflict","title":"Ключ уже относится к другому запросу","status":409,"detail":"Создайте новый ключ для нового намерения."}

Не следует автоматически выдать новый ключ после любого такого отказа и немедленно повторить: человек должен понимать, что это новая операция. Иначе клиент скрыто обходит защиту, превращая ошибку управления намерениями в дополнительное создание. Обычно интерфейс сохраняет введённый документ и предлагает явно решить, исправляется ли прежняя отправка или начинается новая.

Синтаксические и предметные отказы до принятия операции не резервируют ключ в нашем договоре. Если title оказалось пустым, редактор может исправить документ и снова отправить его с этим ключом, пока намерение ещё не принято. Это правило отличается от конфликтующего изменения уже завершённого запроса; граница принятия должна быть описана точно.

Две попытки одновременно

Представим два почти одновременных POST с одним ключом. Нельзя сначала отдельно проверить отсутствие ключа, а потом независимо создать два курса. Оба обработчика способны увидеть пустое место. Реализация нуждается в атомарном резервировании или уникальной записи внутри согласованной транзакции.

Наш сервис допускает одного исполнителя, а второму незавершённому повтору возвращает 409 с типом operation-in-progress. Он может повторить тот же ключ после задержки. Успешный результат появится только после завершения принятой операции. Короткое состояние «в работе» не должно подменяться выдуманным 201 до фактического сохранения курса.

Если исполнитель аварийно завершился, реализация обязана уметь выяснить результат и восстановить запись операции. Этот урок не даёт готовую транзакцию для всех баз и очередей. Он задаёт критерий: после сбоя нельзя произвольно разрешить ещё одно создание, если первая попытка могла завершиться. Механизм восстановления относится к выбранному хранилищу и границам его атомарности.

Внешние побочные действия требуют отдельного внимания. Создание курса в базе и отправка письма не обязаны происходить в одной атомарной среде. Ключ HTTP-запроса сам по себе не гарантирует однократную доставку уведомления. Для нашего начального API создание не запускает внешнюю оплату, почту или webhook, чтобы договор оставался ограниченным понятной записью каталога.

Как клиент сохраняет неопределённую отправку

Интерфейсу полезно различать редактируемую форму и принятый к отправке документ. После нажатия кнопки клиент запоминает ключ и согласованные поля именно этой попытки. Если человек продолжает менять форму, эти значения не должны незаметно заменить содержимое повтора с прежним ключом. Повтор возвращает результат прежнего намерения, а новое создание требует явного нового действия.

Для диагностики можно хранить идентификатор операции, но не превращать ключ в секрет доступа. Он не заменяет личность редактора, не разрешает чтение чужого результата и не должен содержать персональные данные. Проверка области и прав выполняется сервером; устойчивость ключа нужна для различения попыток, а не для обхода авторизации.

Клиент также должен показывать неопределённость честно. После обрыва можно сказать, что результат пока неизвестен и ожидается восстановление прежней операции. Утверждение «курс точно не создан» не соответствует известным данным. Когда повтор вернул сохранённый 201, интерфейс завершает прежнее намерение и показывает c005; он не считает этот ответ доказательством ещё одного нового курса.

Такая модель состояния делает ограничение срока понятным человеку и не прячет распределённый сбой за красивым сообщением об успехе.

Срок хранения и поздний повтор

Успешный результат хранится 24 часа от первого завершения. До истечения срока одинаковый ключ в своей области восстанавливает тот же успех. После истечения запись ключа может быть удалена, и поздний POST способен создать новый курс. Следовательно, нельзя обещать бессрочную дедупликацию или универсальное выполнение «ровно один раз».

Клиент, потерявший связь на несколько дней, не должен слепо повторять старое намерение, полагаясь на забытый ключ. Ему нужно сверить состояние и разрешить неопределённость по дополнительным продуктовым правилам. Срок — не техническая мелочь: от него зависит допустимый интерфейс восстановления.

Теперь в учебном сценарии различите четыре ситуации: тот же вход до завершения, тот же вход после успеха, другой вход с прежним ключом и повтор после срока. Только одна из них возвращает сохранённый 201 по нашему договору. Такие ограничения делают механизм проверяемым и позволяют перейти к чтению больших списков, где идентичность намерения сменяется задачей устойчивого порядка и снимка данных.

Оглавление курса