🌐 Розділ 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).

Пов’язані теми