Что произойдет, если несколько раз вызвать метод PUT на один и тот же эндпоинт?

Ответ

Согласно спецификации HTTP, метод PUT является идемпотентным. Это означает, что несколько идентичных запросов PUT должны оказывать на сервер тот же эффект, что и один запрос.

Типичное поведение REST API:

  1. Первый вызов PUT на /resource/{id}: Если ресурс с таким id существует, он полностью заменяется телом запроса. Если не существует — создается (если серверная логика это позволяет).
  2. Последующие идентичные вызовы: Состояние ресурса после первого успешного вызова не меняется. Сервер должен возвращать тот же успешный статус (200 OK или 204 No Content).

Пример на C#:

// Все три вызова приведут к одному и тому же состоянию ресурса
var userData = new { name = "Alice", role = "Admin" };
var content = new StringContent(JsonSerializer.Serialize(userData), Encoding.UTF8, "application/json");

await httpClient.PutAsync("/api/users/123", content); // Создает или заменяет
await httpClient.PutAsync("/api/users/123", content); // Ничего не меняет
await httpClient.PutAsync("/api/users/123", content); // Ничего не меняет

Критические уточнения:

  • Идемпотентность ≠ атомарность: Между последовательными вызовами PUT другой клиент может изменить ресурс (например, через PATCH). Ваш последующий PUT перезапишет эти изменения, что может привести к потере данных. Для предотвращения используют оптимистическую блокировку (через заголовок If-Match или версию).
  • Побочные эффекты: Идемпотентность относится к состоянию целевого ресурса. Логирование, отправка уведомлений или списание баланса при каждом вызове — это побочные эффекты, нарушающие идемпотентность. Их следует избегать в обработчике PUT.

Ответ 18+ 🔞

Ну вот смотри, про эту вашу идемпотентность все такие умные разговоры ведут, а на деле-то всё просто, как три копейки. Представь себе, что PUT — это такой упоротый перфекционист, который приходит к тебе в квартиру и говорит: «Всё, хуй с горы, я тут теперь хозяин». И делает всё по-своему.

Первый раз он приходит — ха-ха, сука — смотрит, а твоя полка с книгами пустая. Он такой: «А, ну значит, сейчас я её заполню». И ставит туда один том «Войны и мира». Всё, полка теперь в состоянии «том Войны и мира».

Ты его второй раз зовёшь, а он, блядь, опять приходит с ТОЙ ЖЕ САМОЙ книгой. Смотрит на полку — а там уже «Война и мир» стоит. Он такой: «Ну и хуй с ним, пусть так и стоит». И уходит. Полка не изменилась, ей похуй. Третий раз — та же история. Это и есть идемпотентность, ёпта. Сделал раз — и похуй, сколько раз потом ни вызывай, результат для полки один и тот же.

Вот, смотри, как это в коде выглядит, там всё честно:

// Все три вызова приведут к одному и тому же состоянию ресурса
var userData = new { name = "Alice", role = "Admin" };
var content = new StringContent(JsonSerializer.Serialize(userData), Encoding.UTF8, "application/json");

await httpClient.PutAsync("/api/users/123", content); // Создает или заменяет
await httpClient.PutAsync("/api/users/123", content); // Ничего не меняет
await httpClient.PutAsync("/api/users/123", content); // Ничего не меняет

Видишь? Три раза шлём одно и то же. После первого вызова юзер Алиса стала админом. После второго и третьего — она так и осталась админом Алисой, ни хуя не поменялось. Сервер должен тебе в ответ такое же успешное «ок, пошёл нахуй» (200 OK) отправить.

Но вот тут, сука, самый сок, где все обжигаются! Слушай сюда, это важно.

Первое: идемпотентность — это не атомарность, блядь! Это разные вещи, как хуй и палец. Твои три вызова PUT идут не в вакууме. Пока ты второй раз свою Алису слал, какой-нибудь другой распиздяй мог прийти и методом PATCH сменить ей роль на «Гость». А твой второй PUT, идиот, прилетает и нахуй стирает эти изменения, ставя обратно «Admin». Получилась потеря данных, ебать колотить! Чтобы такого не было, умные дяди придумали оптимистичную блокировку — это когда ты вместе с данными шлёшь какую-нибудь версию ресурса или ETag в заголовке If-Match. Если версия не совпала — тебе прилетает ошибка «иди нахуй, тут уже всё поменялось».

Второе: побочные эффекты — это пиздец! Идемпотентность касается только состояния самого ресурса — той самой полки с книгой или юзера Алисы. А если в твоём обработчике PUT, блядь, на каждый вызов пишется лог, отправляется смс админу и списывается рубль с баланса — это пиздец, нарушение всех правил. После первого вызова баланс уже ноль, а ты его ещё два раза списываешь! Клиент-то думает, что он просто «обновляет» данные, а у него деньги хуяк-хуяк и нету. Такую хуйню в PUT делать нельзя категорически. Для этого есть другие методы, POST, например.

Короче, запомни: PUT — это такой упрямый мудак, который приводит мир в точное, заданное тобой состояние. Сколько раз его ни зови — он будет стараться сделать одно и то же. Но мир вокруг него меняется, и другие мудаки тоже хотят своё навести. И если ты не предусмотришь блокировки — будет драка и потеря данных. А если навешаешь на него побочных действий — будет финансовый пиздец. Всё, вопрос закрыт.