← Назад к вопросам

Какие знаешь идемпотентные методы в REST?

1.8 Middle🔥 171 комментариев
#REST API и микросервисы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Идемпотентные методы в REST

Идемпотентность в REST означает, что многократное выполнение одного и того же запроса даёт тот же результат, что и выполнение его один раз. Это критически важно для надежности распределённых систем и обработки сетевых ошибок.

Концепция идемпотентности

Идемпотентный запрос можно безопасно повторять без побочных эффектов. Например, если запрос потеряется в сети, клиент может попробовать снова, и система не будет повреждена.

Идемпотентный запрос:
f(x) = f(f(x)) = f(f(f(x))) = ...

Не идемпотентный запрос:
f(x) ≠ f(f(x)) — каждый вызов может дать другой результат

HTTP методы и идемпотентность

GET — Идемпотентный и безопасный

Запрос только читает данные, не изменяя их на сервере.

// REST контроллер
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(userMapper.toDTO(user));
    }
    
    // Идемпотентно: 100 вызовов = 1 вызов
    // GET /api/v1/users/123
    // GET /api/v1/users/123
    // GET /api/v1/users/123
    // Всегда вернёт одного и того же пользователя с id=123
}

Свойства:

  • ✅ Идемпотентный
  • ✅ Безопасный (не изменяет состояние)
  • Кэшируемый
  • Можно безопасно повторять при ошибке

HEAD — Идемпотентный и безопасный

Аналогичен GET, но без тела ответа. Используется для проверки существования ресурса.

@HeadMapping("/{id}")
public ResponseEntity<Void> checkUserExists(@PathVariable Long id) {
    boolean exists = userService.existsById(id);
    if (exists) {
        return ResponseEntity.ok().build();
    } else {
        return ResponseEntity.notFound().build();
    }
}

// HEAD /api/v1/users/123
// Ответ: 200 OK (или 404)
// Без тела ответа, но статус говорит о наличии ресурса

OPTIONS — Идемпотентный и безопасный

Получение информации о доступных методах для ресурса.

@RequestMapping(
    value = "/{id}",
    method = RequestMethod.OPTIONS
)
public ResponseEntity<Void> getUserOptions(@PathVariable Long id) {
    return ResponseEntity
        .ok()
        .allow(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE)
        .build();
}

// OPTIONS /api/v1/users/123
// Ответ:
// Allow: GET, PUT, DELETE

PUT — Идемпотентный (при правильной реализации)

Полная замена ресурса. Несколько PUT запросов с одинаковыми данными = один PUT запрос.

@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
        @PathVariable Long id,
        @RequestBody UpdateUserRequest request) {
    
    User user = userService.findById(id);
    user.setName(request.getName());
    user.setEmail(request.getEmail());
    user.setAge(request.getAge());
    userService.save(user);
    
    return ResponseEntity.ok(userMapper.toDTO(user));
}

// Идемпотентно:
// PUT /api/v1/users/123
// { "name": "Alice", "email": "alice@test.com", "age": 30 }
// 
// Повторный вызов с теми же данными:
// PUT /api/v1/users/123
// { "name": "Alice", "email": "alice@test.com", "age": 30 }
// Результат: пользователь 123 имеет точно такие же данные (идемпотентно)

Почему PUT идемпотентный:

  • Заменяет ПОЛНОЕ состояние ресурса
  • Новое состояние не зависит от предыдущего
  • Если отправить одни и те же данные дважды, результат одинаковый

DELETE — Идемпотентный (при правильной реализации)

Удаление ресурса. Первый DELETE удаляет, второй тоже возвращает успешный статус (204 или 404).

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    User user = userService.findById(id);
    userService.delete(user);
    return ResponseEntity.noContent().build();  // 204 No Content
}

// Идемпотентно:
// DELETE /api/v1/users/123 -> 204 No Content
// DELETE /api/v1/users/123 -> 204 No Content (уже удалён, но статус успеха)
// DELETE /api/v1/users/123 -> 204 No Content
// 
// Или:
// DELETE /api/v1/users/123 -> 204 No Content
// DELETE /api/v1/users/123 -> 404 Not Found
// Оба ответа указывают, что ресурса нет

PATCH — НЕ идемпотентный (обычно)

Частичное обновление ресурса. Несколько PATCH запросов могут дать разные результаты.

@PatchMapping("/{id}")
public ResponseEntity<UserDTO> partialUpdateUser(
        @PathVariable Long id,
        @RequestBody Map<String, Object> updates) {
    
    User user = userService.findById(id);
    
    // Частичное обновление
    if (updates.containsKey("age")) {
        user.setAge((Integer) updates.get("age"));
    }
    if (updates.containsKey("email")) {
        user.setEmail((String) updates.get("email"));
    }
    
    userService.save(user);
    return ResponseEntity.ok(userMapper.toDTO(user));
}

// НЕ идемпотентно:
// PATCH /api/v1/users/123
// { "age": 31 }  // Увеличиваем возраст на 1
// 
// Повторный вызов:
// PATCH /api/v1/users/123
// { "age": 31 }  // Но сервер может интерпретировать как "increment age"
// Результат: age может стать 32 или оставаться 31 (зависит от реализации)

POST — НЕ идемпотентный (обычно)

Создание нового ресурса. Каждый POST создаёт новый ресурс.

@PostMapping
public ResponseEntity<UserDTO> createUser(@RequestBody CreateUserRequest request) {
    User user = new User();
    user.setName(request.getName());
    user.setEmail(request.getEmail());
    User savedUser = userService.save(user);
    
    return ResponseEntity
        .created(URI.create("/api/v1/users/" + savedUser.getId()))
        .body(userMapper.toDTO(savedUser));
}

// НЕ идемпотентно:
// POST /api/v1/users
// { "name": "Alice", "email": "alice@test.com" }
// Ответ: { "id": 1, "name": "Alice", "email": "alice@test.com" }
// 
// Повторный вызов с теми же данными:
// POST /api/v1/users
// { "name": "Alice", "email": "alice@test.com" }
// Ответ: { "id": 2, "name": "Alice", "email": "alice@test.com" }
// Создан НОВЫЙ пользователь с другим id

Матрица HTTP методов

Метод | Идемпотентный | Безопасный | Кэшируемый | Тело ответа
---|---|---|---|---
GET   | ✅            | ✅         | ✅         | ✅
HEAD  | ✅            | ✅         | ✅         | ❌
OPTIONS | ✅           | ✅         | ❌         | ❌
TRACE | ✅            | ✅         | ❌         | ✅
PUT   | ✅            | ❌         | ❌         | ✅
DELETE | ✅            | ❌         | ❌         | ❌
POST  | ❌            | ❌         | Условно    | ✅
PATCH | ❌ (обычно)    | ❌         | ❌         | ✅

Практические паттерны обработки идемпотентности

1. Idempotency-Key для POST запросов

Добавляем уникальный ключ для отслеживания дубликатов.

@PostMapping
public ResponseEntity<UserDTO> createUser(
        @RequestBody CreateUserRequest request,
        @RequestHeader("Idempotency-Key") String idempotencyKey) {
    
    // Проверяем, не обработан ли этот ключ ранее
    Optional<User> existingRequest = userService.findByIdempotencyKey(idempotencyKey);
    if (existingRequest.isPresent()) {
        return ResponseEntity.ok(userMapper.toDTO(existingRequest.get()));
    }
    
    // Создаём новый ресурс
    User user = new User();
    user.setName(request.getName());
    user.setIdempotencyKey(idempotencyKey);
    User savedUser = userService.save(user);
    
    return ResponseEntity.created(
        URI.create("/api/v1/users/" + savedUser.getId())
    ).body(userMapper.toDTO(savedUser));
}

// Клиент отправляет:
// POST /api/v1/users
// Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
// { "name": "Alice", "email": "alice@test.com" }
//
// При повторе с тем же ключом сервер вернёт тот же результат

2. Использование версий (optimistic locking)

@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
        @PathVariable Long id,
        @RequestBody UpdateUserRequest request,
        @RequestParam Long version) {
    
    User user = userService.findById(id);
    
    // Проверяем версию
    if (!user.getVersion().equals(version)) {
        return ResponseEntity.status(409).build();  // Conflict
    }
    
    user.setName(request.getName());
    user.setVersion(user.getVersion() + 1);
    userService.save(user);
    
    return ResponseEntity.ok(userMapper.toDTO(user));
}

3. Обработка ошибок и повторные попытки

@Component
public class IdempotentRestTemplate {
    private final RestTemplate restTemplate;
    
    public ResponseEntity<String> executeIdempotent(
            String url, 
            HttpMethod method,
            String idempotencyKey) {
        
        HttpHeaders headers = new HttpHeaders();
        headers.set("Idempotency-Key", idempotencyKey);
        HttpEntity<?> request = new HttpEntity<>(headers);
        
        // Повторяем при сетевых ошибках
        for (int attempt = 0; attempt < 3; attempt++) {
            try {
                return restTemplate.exchange(url, method, request, String.class);
            } catch (HttpServerErrorException | ResourceAccessException e) {
                if (attempt == 2) throw e;
                Thread.sleep(1000 * (attempt + 1));  // Exponential backoff
            }
        }
        throw new RuntimeException("Failed after retries");
    }
}

Рекомендации

  1. GET, HEAD, OPTIONS, PUT, DELETE — всегда идемпотентны (спроектируй так)
  2. POST, PATCH — по умолчанию не идемпотентны
  3. Для POST с идемпотентностью используй Idempotency-Key заголовок
  4. Возвращай консистентные статусы для повторяющихся запросов
  5. Документируй идемпотентность в API спецификации
  6. Реализуй deduplication на сервере для критичных операций
  7. Используй 409 Conflict если запрос конфликтует с предыдущим

Идемпотентность — ключевой принцип для создания надежных REST API, устойчивых к сетевым сбоям и дублирующимся запросам.

Какие знаешь идемпотентные методы в REST? | PrepBro