По семантике HTTP методы GET, PUT и DELETE идемпотентны, POST — нет. Повтор POST после таймаута без ключа легко создаёт вторую операцию: запись могла пройти, а ответ потеряться.
API и RPC http собеседование
Идемпотентный метод: повтор запроса с тем же смыслом не меняет состояние сверх первого успеха. Безопасный метод (GET, HEAD) ещё и без побочных эффектов. Поэтому GET можно кэшировать.
Это контракт протокола, а не «мой хендлер случайно так написан». Если POST создаёт заказ, протокол не обещает, что второй такой же POST безвреден.
Сеть рвётся в худший момент. Сервер записал и упал до ответа. Прокси вернул 502. Клиентский таймаут сработал, пока handler ещё коммитил. Клиент не знает, прошла операция или нет. Слепой ретрай POST — два списания. Таймаут ≠ «сервер точно ничего не записал».
Методы
- GET, HEAD — безопасные. Повтор не создаёт заказ. Кэш и прокси на это опираются.
- PUT — «сделай ресурс таким»: повтор с тем же телом даёт тот же результат.
- DELETE — повтор после успеха часто 404. Состояние «ресурса нет» то же.
- POST — «создай / запусти действие». Без ключа повтор опасен.
- PATCH не обязан быть идемпотентным. Если делаете его таковым — опишите в контракте.
Idempotency-Key
Idempotency-Key простыми словами: клиент даёт уникальный номер операции. Сервер запоминает этот номер и результат. Повтор с тем же номером — тот же ответ, вторая проводка не создаётся.
Пример. Оплата: клиент шлёт POST /charge с ключом abc-123. Сервер списал деньги и упал до ответа. Клиент не знает, прошло или нет. Шлёт тот же POST с тем же ключом. Сервер видит abc-123 в таблице — возвращает старый успех, второй раз не списывает.
func charge(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("Idempotency-Key")
if key == "" {
http.Error(w, "Idempotency-Key required", http.StatusBadRequest)
return
}
if resp, ok := store.Get(r.Context(), userID(r), key); ok {
writeJSON(w, resp.Status, resp.Body) // тот же ответ
return
}
result, err := billing.Charge(r.Context(), parse(r.Body))
store.Put(userID(r), key, result) // TTL сутки, скоуп пользователь+операция
writeJSON(w, http.StatusCreated, result)
}
Ключ на POST: повтор после таймаута возвращает сохранённый результат, а не вторую проводку.
Ключ обычно UUID. Сервер хранит ключ → результат. Или «in-progress», чтобы параллельный повтор не начал вторую работу. Ключи живут ограниченно. Скоуп — пользователь плюс операция. Иначе один UUID на весь сервис столкнёт чужие запросы.
Ретраи безопасны, если метод идемпотентен или есть ключ. Или натуральный UNIQUE в БД: order_id. PUT по каноническому URL часто обходится без заголовка.
Не путайте идемпотентность с «безопасностью GET». PUT меняет состояние, но повтор того же PUT не должен удваивать эффект. Очереди at-least-once дают ту же задачу: сообщение могут доставить дважды. Идемпотентный consumer и Idempotency-Key — один навык, разные входы.
Типичная ошибка
Ретраить POST после таймаута «потому что ошибки не было в логах клиента». Запись могла пройти. Вторая ошибка — считать любой JSON-метод идемпотентным или путать идемпотентность с безопасностью GET.
На собеседовании
PUT по протоколу идемпотентен, POST — нет без ключа. Idempotency-Key лечит повтор после таймаута. Свяжите с at-least-once очередью: без идемпотентного консьюмера будет то же удвоение.