Является ли PUT-запрос идемпотентным?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Является ли PUT-запрос идемпотентным?
Да, PUT-запрос идемпотентен по спецификации HTTP.
Что такое идемпотентность?
Идемпотентность — это свойство операции, при котором выполнение её несколько раз даёт такой же результат, что и выполнение один раз.
f(f(x)) = f(x)
Примеры идемпотентных операций:
x + 0 = x(идемпотент)x * 1 = x(идемпотент)x + 5(НЕ идемпотент)
PUT-запрос идемпотентен
По стандарту HTTP PUT полностью заменяет ресурс. Если вызвать его дважды с одинаковыми данными, результат будет идентичен первому вызову.
// Запрос 1: PUT /api/v1/users/123
{
"name": "John",
"email": "john@example.com",
"age": 30
}
// Результат: пользователь с id=123 обновлён
// { id: 123, name: "John", email: "john@example.com", age: 30 }
// Запрос 2: PUT /api/v1/users/123 (ТОТЖЕ запрос)
{
"name": "John",
"email": "john@example.com",
"age": 30
}
// Результат: ИДЕНТИЧЕН запросу 1
// { id: 123, name: "John", email: "john@example.com", age: 30 }
// Запрос 3: PUT /api/v1/users/123 (тот же запрос)
// Результат: СНОВА идентичен
// { id: 123, name: "John", email: "john@example.com", age: 30 }
Пример в Node.js
// users.controller.ts
export class UsersController {
// PUT — полная замена ресурса (идемпотентен)
@Put('/users/:id')
updateUserFully(@Param('id') id: string, @Body() dto: UpdateUserDto) {
return this.usersService.replace(id, dto);
}
// PATCH — частичное обновление (НЕ всегда идемпотентен)
@Patch('/users/:id')
updateUserPartially(@Param('id') id: string, @Body() dto: Partial<UpdateUserDto>) {
return this.usersService.update(id, dto);
}
}
// users.service.ts
export class UsersService {
// PUT: полная замена
replace(id: string, dto: UpdateUserDto) {
// DELETE старые данные
// INSERT новые данные
// Или UPDATE все поля (заменить полностью)
const user = {
id,
name: dto.name,
email: dto.email,
age: dto.age,
// Все остальные поля сбросятся на дефолт или будут заменены
};
return this.usersRepository.save(user);
}
// PATCH: частичное обновление (может быть неидемпотентным)
update(id: string, dto: Partial<UpdateUserDto>) {
// UPDATE поля, которые пришли в dto
// Остальные поля не трогаем
return this.usersRepository.update(id, dto);
}
}
Сравнение: PUT vs PATCH vs POST
PUT — идемпотентен
// Трёхкратный вызов одного и того же PUT
PUT /api/v1/users/123 { name: "John", age: 30 }
PUT /api/v1/users/123 { name: "John", age: 30 }
PUT /api/v1/users/123 { name: "John", age: 30 }
// Результат: 100% идентичен во всех 3 случаях
PATCH — может быть НЕ идемпотентен
// Первый вызов: увеличить age на 1
PATCH /api/v1/users/123 { increment_age: 1 }
// age = 30 + 1 = 31
// Второй вызов: ТОТЖЕ запрос
PATCH /api/v1/users/123 { increment_age: 1 }
// age = 31 + 1 = 32 (ДРУГОЙ результат!)
// Третий вызов
PATCH /api/v1/users/123 { increment_age: 1 }
// age = 32 + 1 = 33 (ещё ДРУГОЙ результат!)
// PATCH НЕ идемпотентен в этом случае!
POST — НЕ идемпотентен
// Первый вызов
POST /api/v1/users { name: "John" }
// Результат: создан пользователь с id=1
// Второй вызов
POST /api/v1/users { name: "John" }
// Результат: создан НОВЫЙ пользователь с id=2
// Третий вызов
POST /api/v1/users { name: "John" }
// Результат: создан НОВЫЙ пользователь с id=3
// POST создаёт новый ресурс каждый раз — НЕ идемпотентен
Реальный пример: обновление профиля
// PUT — правильно
router.put('/api/v1/profile/:id', async (req, res) => {
const { name, email, age, bio } = req.body;
// Заменяем ВСЕ поля профиля
const profile = await Profile.findByIdAndUpdate(
req.params.id,
{
name,
email,
age,
bio,
// Если в запросе не пришло поле — оно будет пусто
// Или используй $set для замены только переданных полей
},
{ new: true, overwrite: true } // overwrite = полная замена
);
res.json(profile);
});
// Три идентичных запроса:
PUT /api/v1/profile/123
{ name: "Alice", email: "alice@example.com", age: 28, bio: "Developer" }
PUT /api/v1/profile/123
{ name: "Alice", email: "alice@example.com", age: 28, bio: "Developer" }
PUT /api/v1/profile/123
{ name: "Alice", email: "alice@example.com", age: 28, bio: "Developer" }
// Результат всегда одинаков ✓ ИДЕМПОТЕНТЕН
Почему PUT идемпотентен?
- Полная замена — PUT заменяет весь ресурс на новые данные
- Без условной логики — не зависит от текущего состояния
- Воспроизводимый результат — один и тот же вход = один и тот же выход
Важное замечание: правильная реализация
ПУТ БУДЕТ идемпотентным только если:
// ✓ ПРАВИЛЬНО — полная замена всех полей
router.put('/users/:id', (req, res) => {
const user = {
id: req.params.id,
name: req.body.name,
email: req.body.email,
age: req.body.age,
// Все поля явно переопределены
};
return db.save(user);
});
// ✗ НЕПРАВИЛЬНО — добавляет данные
router.put('/users/:id', (req, res) => {
const user = db.findById(req.params.id);
user.tags.push(...req.body.newTags); // Добавляем!
return db.save(user);
// Второй вызов добавит ещё теги — НЕ идемпотентен!
});
// ✗ НЕПРАВИЛЬНО — изменяет на основе текущего состояния
router.put('/users/:id', (req, res) => {
const user = db.findById(req.params.id);
user.age += 1; // Увеличиваем!
return db.save(user);
// Второй вызов увеличит возраст ещё раз — НЕ идемпотентен!
});
Практические советы
- PUT = полная замена всех полей (или явно заданных полей)
- PATCH = частичное обновление (может быть неидемпотентным)
- POST = создание нового ресурса (ВСЕГДА неидемпотентен)
- Всегда проверяй, что PUT-эндпоинт работает идемпотентно
- Документируй, какие поля требуются в PUT запросе
- Возвращай 200 OK если ресурс уже в нужном состоянии
- Возвращай 201 Created только если создан новый ресурс (в PUT редко)
Заключение
Да, PUT-запрос идемпотентен по спецификации HTTP. Это означает, что отправка одного и того же PUT запроса несколько раз должна дать одинаковый результат. Это сделано для надёжности: если сетевое соединение нестабильно и запрос был отправлен дважды, результат будет корректным. Это ключевое отличие от POST (создание — неидемпотентно) и от PATCH (может быть неидемпотентным).