🌐 Раздел 6 · Вопрос #6

Какие HTTP методы идемпотентны

По спецификации HTTP (RFC 9110) все методы делятся на идемпотентные и неидемпотентные:


🟢 Junior Level

Идемпотентный метод в протоколе HTTP — это метод, повторное выполнение которого с теми же параметрами оставляет состояние сервера точно таким же, как и после первого выполнения.

По спецификации HTTP (RFC 9110) все методы делятся на идемпотентные и неидемпотентные:

Сводная классификация методов

HTTP Метод Безопасный (Safe) Идемпотентный (Idempotent) Семантическое назначение
GET ✅ Да ✅ Да Получение представления ресурса (чтение).
HEAD ✅ Да ✅ Да Получение заголовков без тела ответа.
OPTIONS ✅ Да ✅ Да Запрос списка поддерживаемых сервером методов и CORS-параметров.
TRACE ✅ Да ✅ Да Эхо-тестирование пути запроса (диагностика).
PUT ❌ Нет ✅ Да Полная замена существующего ресурса или создание по фиксированному URI.
DELETE ❌ Нет ✅ Да Удаление ресурса.
POST ❌ Нет ❌ Нет Создание подчиненного ресурса, обработка данных, запуск команд.
PATCH ❌ Нет ❌ Зависит Частичное изменение ресурса (по RFC 5789 не гарантируется).

Почему одни методы идемпотентны, а другие нет

  • GET /users/42 — сколько раз ни запрашивай, база данных не изменится.
  • PUT /users/42 с телом {"name": "Ivan"} — первый раз перезапишет имя на “Ivan”, повторные запросы снова запишут “Ivan”. Состояние базы данных то же самое.
  • DELETE /users/42 — первый раз удалит пользователя. Повторные запросы убедятся, что пользователя нет. Состояние (пользователь отсутствует) не изменилось.
  • POST /orders — отправка формы заказа 3 раза подряд приведет к созданию 3 независимых заказов и трехкратному списанию денег.

🟡 Middle Level

Связь идемпотентности с кэшированием (RFC 9111)

Идемпотентность и безопасность напрямую определяют работу промежуточных кэширующих серверов (CDN, Cloudflare, NGINX, Squid):

  1. Кэширование Safe методов: Запросы GET и HEAD кэшируются по умолчанию при наличии соответствующих заголовков (Cache-Control, ETag, Last-Modified).
  2. Инвалидация кэша неидемпотентными и мутирующими методами: Согласно RFC 9111, успешные ответы на запросы POST, PUT, PATCH и DELETE обязаны инвалидировать (сбрасывать) кэшированные представления для целевого URI (Request-URI), а также для URI, указанных в заголовках Location и Content-Location.

Оптимистическая блокировка через HTTP Preconditions (RFC 9110)

Для предотвращения проблемы «потерянного обновления» (Lost Update Problem) при параллельных PUT и PATCH запросах применяются условные заголовки (Conditional Headers):

PUT /api/v1/products/100 HTTP/1.1
If-Match: "v3-etag-hash"
Content-Type: application/json

{
  "price": 199.99
}
  • Если версия ресурса в БД совпадает с "v3-etag-hash", сервер обновляет сущность и возвращает 200 OK с новым ETag: "v4-etag-hash".
  • Если другой клиент успел обновить сущность раньше, сервер возвращает статус 412 Precondition Failed, защищая данные от перезаписи устаревшим состоянием.

Семантика PATCH: почему спецификация не гарантирует идемпотентность

Метод PATCH (RFC 5789) предназначен для частичной модификации. Идемпотентность зависит от характера применяемой дельты:

// 1. Идемпотентный PATCH (JSON Patch RFC 6902)
[
  { "op": "replace", "path": "/email", "value": "new@corp.com" }
]

// 2. НЕИДЕМПОТЕНТНЫЙ PATCH (JSON Patch RFC 6902)
[
  { "op": "add", "path": "/roles/-", "value": "ADMIN" }
]

В первом случае повтор запроса оставляет email равным "new@corp.com". Во втором случае оператор add с путем /- добавляет элемент в конец массива; повторение вызова приведет к массиву ролей ["USER", "ADMIN", "ADMIN", "ADMIN"].


🔴 Senior Level

Идемпотентность в микросервисной архитектуре и Service Mesh (Istio / Envoy)

В распределенных инфраструктурах знание идемпотентности критично для автоматической устойчивости к сбоям (Retry Policy):

Автоматический повтор запросов (Envoy VirtualService)

Прокси-сервер (Envoy) в Service Mesh может безопасно повторять запросы только для методов с известной идемпотентностью. Попытка автоматического ретрая для POST может привести к дублированию платежей или заказов.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: order-service-retry
spec:
  hosts:
  - order-service
  http:
  - match:
    - uri:
        prefix: /api/v1/orders
      method:
        exact: GET
    route:
    - destination:
        host: order-service
    retries:
      attempts: 3
      perTryTimeout: 500ms
      retryOn: "5xx,connect-failure,refused-stream"

Реализация Resilience4j Retry с фильтрацией по идемпотентности в Spring Boot 3

В микросервисах на Spring Boot межсервисные вызовы через RestClient или WebClient настраиваются так, чтобы повторы выполнялись только для безопасных и идемпотентных методов:

package com.example.client;

import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.time.Duration;
import java.util.Set;

@Component
public class IdempotentRetryInterceptor implements ClientHttpRequestInterceptor {

    private static final Set<HttpMethod> IDEMPOTENT_METHODS = Set.of(
            HttpMethod.GET,
            HttpMethod.HEAD,
            HttpMethod.OPTIONS,
            HttpMethod.PUT,
            HttpMethod.DELETE
    );

    private final Retry retry;

    public IdempotentRetryInterceptor() {
        RetryConfig config = RetryConfig.<ClientHttpResponse>custom()
                .maxAttempts(3)
                .waitDuration(Duration.ofMillis(200))
                .retryExceptions(IOException.class) // Повтор только при сетевых сбоях (I/O)
                .build();
        this.retry = Retry.of("idempotentHttpRetry", config);
    }

    @Override
    public ClientHttpResponse intercept(org.springframework.http.HttpRequest request,
                                        byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {

        // Неидемпотентные запросы (POST, PATCH) выполняются без автоматического retry
        if (!IDEMPOTENT_METHODS.contains(request.getMethod())) {
            return execution.execute(request, body);
        }

        // Идемпотентные запросы оборачиваются в Resilience4j Retry
        try {
            return Retry.decorateCheckedSupplier(retry, () -> execution.execute(request, body)).get();
        } catch (Throwable throwable) {
            if (throwable instanceof IOException ioException) {
                throw ioException;
            }
            throw new RuntimeException("HTTP retry execution failed", throwable);
        }
    }
}

Допустимы ли побочные эффекты (Side Effects) у идемпотентных методов?

Спецификация RFC 9110 (Раздел 9.2.2) прямо разъясняет фундаментальный вопрос архитектуры:

“The definition of idempotent methods does not prevent an implementation from recording each request, e.g., in an access log, or from taking actions that affect the system outside of the resource state.”

Это означает:

  1. Запись строчки в Audit Log или access.log при GET или DELETE запросе не нарушает идемпотентность.
  2. Инкремент метрики Prometheus http_requests_total не нарушает идемпотентность.
  3. Идемпотентность оценивается исключительно по целевому состоянию ресурса, которым управляет метод.

🎯 Шпаргалка для интервью

30-секундный ответ

  • Идемпотентные методы (RFC 9110): GET, HEAD, OPTIONS, TRACE, PUT, DELETE. Повторный вызов оставляет ресурс в том же состоянии.
  • Неидемпотентные методы: POST (создание/процессинг), PATCH (по RFC 5789 идемпотентность не гарантируется).
  • Safe ⊂ Idempotent: Все безопасные методы (GET, HEAD, OPTIONS) не меняют состояние сервера и являются идемпотентными. PUT и DELETE меняют состояние, но повторные вызовы детерминированы.
  • Инфраструктурное значение: Service Mesh (Envoy/Istio) и HTTP-клиенты могут безопасно применять автоматические повторы (retries) при сбоях сети только к идемпотентным методам.

4 каверзных вопроса с ответами

  1. Является ли метод PATCH идемпотентным по стандарту? Ответ: Нет, по спецификации RFC 5789 метод PATCH не гарантирует идемпотентность. Она зависит от применяемых инструкций: замена значения (replace) идемпотентна, а добавление элемента в список (add в JSON Patch) или инкремент значения при каждом вызове изменяют состояние ресурса.

  2. Клиент вызвал DELETE /orders/5 и получил 204 No Content. Через секунду вызвал снова и получил 404 Not Found. Нарушена ли идемпотентность? Ответ: Нет, не нарушена. Идемпотентность гарантирует одинаковое состояние системы (ресурс с ID 5 отсутствует в базе данных), а не идентичность статус-кодов ответов.

  3. Что происходит с кэшем при выполнении методов PUT или DELETE? Ответ: По спецификации RFC 9111 любой успешный запрос, использующий небезопасный метод (PUT, POST, DELETE, PATCH), обязан инвалидировать (удалить из кэша) сохраненный ответ для целевого URI (Request-URI), а также URI из заголовков Location и Content-Location.

  4. Почему генерация нового audit_log_id в таблице аудита при вызове DELETE /users/1 не ломает идемпотентность? Ответ: RFC 9110 разделяет состояние целевого ресурса и побочные эффекты инфраструктуры (логирование, сбор метрик, мониторинг). Состояние ресурса User не меняется при повторе (он остается удаленным), поэтому требование спецификации соблюдено.

Красные флаги (чего категорически нельзя говорить)

  • ❌ «Все HTTP-методы идемпотентны, кроме POST» (PATCH по RFC не гарантирует идемпотентность).
  • ❌ «DELETE не идемпотентен, потому что коды ответа 200 и 404 разные» (путаница между статус-кодом ответа и состоянием ресурса на сервере).
  • ❌ «В Service Mesh можно ставить автоматический retry на любые методы, включая POST» (приведет к дублированию транзакций и данных).
  • ❌ «Идемпотентность запрещает писать логи в файл при выполнении запроса» (прямо разрешено в RFC 9110 §9.2.2).

Связанные темы