Какие 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):
- Кэширование Safe методов: Запросы
GETиHEADкэшируются по умолчанию при наличии соответствующих заголовков (Cache-Control,ETag,Last-Modified). - Инвалидация кэша неидемпотентными и мутирующими методами:
Согласно 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.”
Это означает:
- Запись строчки в Audit Log или
access.logприGETилиDELETEзапросе не нарушает идемпотентность. - Инкремент метрики Prometheus
http_requests_totalне нарушает идемпотентность. - Идемпотентность оценивается исключительно по целевому состоянию ресурса, которым управляет метод.
🎯 Шпаргалка для интервью
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 каверзных вопроса с ответами
-
Является ли метод PATCH идемпотентным по стандарту? Ответ: Нет, по спецификации RFC 5789 метод
PATCHне гарантирует идемпотентность. Она зависит от применяемых инструкций: замена значения (replace) идемпотентна, а добавление элемента в список (addв JSON Patch) или инкремент значения при каждом вызове изменяют состояние ресурса. -
Клиент вызвал
DELETE /orders/5и получил 204 No Content. Через секунду вызвал снова и получил 404 Not Found. Нарушена ли идемпотентность? Ответ: Нет, не нарушена. Идемпотентность гарантирует одинаковое состояние системы (ресурс с ID 5 отсутствует в базе данных), а не идентичность статус-кодов ответов. -
Что происходит с кэшем при выполнении методов PUT или DELETE? Ответ: По спецификации RFC 9111 любой успешный запрос, использующий небезопасный метод (
PUT,POST,DELETE,PATCH), обязан инвалидировать (удалить из кэша) сохраненный ответ для целевого URI (Request-URI), а также URI из заголовковLocationиContent-Location. -
Почему генерация нового
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).