Идемпотентность операций
API уже умеет объяснять известный успех и известный отказ. Сложнее случай, когда ответ вообще не дошёл. Клиент отправил изменение, соединение оборвалось, и неизвестно, успел ли сервер сохранить данные. Идемпотентность помогает рассуждать о повторе: несколько одинаковых операций имеют тот же запрошенный эффект, что одна.
В этом уроке рассмотрим чтение, полную замену, удаление и создание. Результат — вы сможете объяснить, почему повтор DELETE может получить другой статус, а повтор POST способен создать дубликат. Сценарии являются моделью работы каталога, без реальной передачи сообщений. Правило повторения оцениваем по состоянию ресурса, а не по сходству строк ответа.
Полная замена известного представления
Курс c001 первоначально имеет title «Современный JavaScript». Редактор хочет получить определённое конечное состояние и отправляет полный документ PUT. Для первой части рассуждения считаем, что конкурентных изменений нет; обязательное условие версии добавим отдельно в уроке 16.
PUT /api/courses/c001 HTTP/1.1
Host: catalog.example
Content-Type: application/json
Accept: application/json, application/problem+json
{"title":"JavaScript для веба","topic":"frontend","lessons":40}
После принятия представление c001 имеет отправленные значения. Если точно тот же запрос применить ещё раз, конечные редактируемые поля будут такими же. Это и есть нужный эффект повторяемости: одна замена и две одинаковые замены приводят к одному желаемому состоянию в описанной ситуации.
Внутренний журнал может содержать две записи обращения. Сервер способен учитывать отдельные попытки и технические сведения. Идемпотентность не обещает, что все внутренние байты системы останутся идентичными. Она относится к запрошенному эффекту операции. Если назвать журнал доказательством «PUT не идемпотентен», мы смешаем предметное действие и учёт запросов.
Ответы тоже могут различаться. Между попытками сервис может менять дату сообщения или служебные поля. Для другого допускающего создание API первый PUT способен получить 201, а последующий — статус замены. Наш сервис не создаёт через PUT, но общая возможность показывает, почему свойство метода нельзя определить только по совпадению кодов. Идемпотентные методы в HTTP.
Удаление остаётся удалением
Теперь удалим c004 в отдельном сценарии, снова начиная со снимка s1. Первая успешная попытка оставляет каталог без этой записи и получает предусмотренный 204 без тела. Если клиент потерял ответ и повторил DELETE, записи уже нет; по нашему договору сервер возвращает 404.
До первой попытки: c001, c002, c003, c004.
После первой попытки: c001, c002, c003.
После повторной попытки: c001, c002, c003.
Список иллюстрирует предметное состояние, а не вывод работающего сервера. Результат одного удаления и двух одинаковых удалений совпадает: c004 отсутствует. Различные статусы не нарушают идемпотентность. Вторая попытка сообщает текущее отсутствие цели, но не восстанавливает запись и не удаляет случайно другой курс.
Клиенту важно интерпретировать повтор в контексте своего намерения. Если после неопределённости цель точно отсутствует, требуемый эффект удаления достигнут. Это не повод объявлять любую первую 404 успешным удалением: возможно, человек ошибся адресом. Контекст действия помогает объяснить состояние, не меняя общий смысл статуса.
Безопасность и идемпотентность различаются. DELETE идемпотентен, но всё равно запрашивает изменение. Его нельзя выполнять при обычном предварительном чтении ссылки. GET в нашем каталоге безопасен и также идемпотентен по запрошенному эффекту, поскольку чтение не изменяет редактируемые поля.
Чтение способно вернуть новые данные
Представим два GET к c001 с паузой. Между ними редактор изменил название. Второй ответ содержит новое title, хотя запросы одинаковы. GET не перестал быть идемпотентным: его запрошенный эффект — получение представления, а не фиксация вечного значения. Текущее состояние ресурса может меняться независимо от чтения.
Это важная граница для кеша. Идемпотентный метод не означает, что любой ответ можно хранить бессрочно или считать неизменным. Политика свежести и валидаторы решают отдельную задачу. Когда клиенту нужна неизменная версия списка, мы позже введём снимок, а не будем выводить постоянство данных из имени GET.
Повтор чтения проще после обрыва, но он тоже требует разумной политики задержек. Если сервис перегружен, мгновенное бесконечное повторение только увеличит нагрузку. Ограничение количества попыток и учёт Retry-After относятся к стратегии клиента; в нашем уроке речь о семантической допустимости повтора, а не о бесконечном цикле запросов.
Создание без защиты от дубликата
POST к коллекции создаёт новый серверный id. Одна попытка с title «HTTP и API» может создать c005. Если ответ потерян и клиент отправил тот же POST ещё раз, сервер способен создать c006 с теми же предметными полями. Это другой запрошенный эффект: в каталоге уже две новые записи.
Один POST: добавлен c005.
Два независимых POST: добавлены c005 и c006.
Одинаковое тело не доказывает, что человек хотел одну операцию. Два редактора могут сознательно создавать похожие курсы, либо один редактор действительно хочет две записи. Поэтому сервер не должен автоматически дедуплицировать по совпадению title и lessons без дополнительного договора. Сравнение предметных данных и идентичность намерения — разные вопросы.
После потери ответа клиент не знает, какой из сценариев произошёл. Сообщение «не удалось создать» может быть ложным, потому что c005 уже существует. Слепой повтор может усугубить неопределённость дубликатом. Следующий урок добавит прикладной ключ одного намерения, с областью действия и сроком хранения результата.
Присваивание и увеличение
Сравните два возможных прикладных действия над lessons. Первое устанавливает значение 40, второе увеличивает текущее значение на один. При одинаковом исходном числе повтор первого действия сохраняет тот же итог, а повтор второго даёт дополнительное увеличение. Сходный объект JSON в запросе не делает эти действия семантически одинаковыми.
Наш PUT использует полное присваивание редактируемого представления. Если позднее появится команда увеличения, её правила повтора придётся определить отдельно. Нельзя скопировать обработчик PUT и назвать новую операцию идемпотентной, только потому что она использует тот же ресурс курса. Предметное действие является основой рассуждения.
Также различайте повтор прежнего документа и новое редактирование. Если человек после успеха меняет число уроков на 41, это новое намерение, хотя путь прежний. Клиент вправе отправить его после соответствующего чтения и проверки версии. Механизм повтора должен хранить исходный документ отдельно от текущих вводимых значений, чтобы случайное изменение формы не превратилось в переотправку другой операции под прежним основанием.
Таким образом, точность клиентского состояния поддерживает семантику сервера. Адрес и метод ещё не рассказывают всю историю: нужны намерение, документ и известный исход предыдущей попытки.
Конкурентное изменение — другая проблема
Вернёмся к PUT. Его повторяемость не даёт редактору право перезаписать более новую работу другого человека. Если между попытками кто-то изменил lessons, прежний полный документ способен вернуть старое число. Без условия версии идемпотентный метод всё равно может привести к потере чужого изменения.
Позже используем If-Match: сервер применит документ только к ожидаемой версии. Тогда повтор после уже принятого изменения может получить 412, потому что валидатор устарел. Это совместимо с идемпотентностью и не означает, что первая попытка обязательно провалилась. Клиент сверяет текущее представление и свой предполагаемый результат.
Получается две независимые задачи: повтор одного намерения после неизвестного исхода и согласование изменения с текущим состоянием. Ключ POST решает первую в объявленной области, условие PUT — вторую. Ни один механизм не следует рекламировать как автоматическое решение всех ошибок распределённой системы.
Теперь для каждой операции запишите состояние после одной и двух одинаковых попыток. Не сравнивайте только ответы: сравните число записей, идентичность цели и редактируемые значения. Такой разбор помогает выбрать допустимую реакцию на обрыв и увидеть, где нужен дополнительный прикладной договор. Его начнём с повторяемого создания курса.