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

Является ли GET-запрос идемпотентным?

2.0 Middle🔥 132 комментариев
#API и сетевые протоколы

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

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

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

Является ли GET-запрос идемпотентным?

Да, GET-запрос является идемпотентным. Идемпотентный запрос — это запрос, который при повторном выполнении дает тот же результат, не изменяя состояние сервера. GET запрос всегда должен только читать данные, не изменяя их.

Определение идемпотентности

Идемпотентный запрос — это запрос, который:

  • При повторном выполнении не изменяет состояние сервера
  • Возвращает одинаковый результат при повторном выполнении
  • Безопасен для повтора (retry)

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

GET    — Идемпотентный (только читает)
HEAD   — Идемпотентный (как GET, но без тела)
OPTIONS — Идемпотентный (только информация)
PUT    — Идемпотентный (заменяет целиком)
DELETE — Идемпотентный (удаление одного ресурса)
POST   — НЕ идемпотентный (создаёт новый ресурс)
PATCH  — НЕ идемпотентный (может различаться)

Примеры идемпотентных GET запросов

// Пример 1: Получение пользователя
GET /users/123

// Первый запрос: возвращает { id: 123, name: 'John' }
// Второй запрос: возвращает { id: 123, name: 'John' }
// Третий запрос: возвращает { id: 123, name: 'John' }
// Всегда один и тот же результат

Примеры с Express

const express = require('express');
const app = express();

// GET запрос — идемпотентный
app.get('/users/:id', (req, res) => {
  const user = db.findUser(req.params.id);
  res.json(user); // Состояние сервера не изменилось
});

// POST запрос — НЕ идемпотентный
app.post('/users', (req, res) => {
  const newUser = { id: uuid(), ...req.body };
  db.saveUser(newUser); // Состояние сервера изменилось!
  res.json(newUser);
});

// PUT запрос — идемпотентный
app.put('/users/:id', (req, res) => {
  const user = db.updateUser(req.params.id, req.body);
  res.json(user);
});

// DELETE запрос — идемпотентный
app.delete('/users/:id', (req, res) => {
  db.deleteUser(req.params.id);
  res.status(204).send();
});

Когда GET может быть НЕ идемпотентным (плохо!)

// ПЛОХО! Это нарушает правила HTTP
app.get('/users/:id/increment-view-count', (req, res) => {
  const user = db.findUser(req.params.id);
  user.viewCount++; // Изменяем состояние! ПЛОХО!
  db.saveUser(user);
  res.json(user);
});

// Первый GET: viewCount = 1
// Второй GET: viewCount = 2
// Третий GET: viewCount = 3
// Результаты разные — это НЕ идемпотентно!

// ПРАВИЛЬНОЕ решение — использовать POST
app.post('/users/:id/view', (req, res) => {
  const user = db.findUser(req.params.id);
  user.viewCount++;
  db.saveUser(user);
  res.json(user);
});

Идемпотентность и побочные эффекты

// Логирование в GET — ОК (не изменяет состояние основных данных)
app.get('/products/:id', (req, res) => {
  const product = db.getProduct(req.params.id);
  
  // Побочный эффект: логирование
  logger.info(`Product ${req.params.id} viewed`); // OK
  
  // Побочный эффект: увеличение счетчика
  db.incrementViewCount(req.params.id); // ПЛОХО! Изменяет состояние
  
  res.json(product);
});

Почему это важно

// Для retry логики
async function fetchUserWithRetry(userId, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      // GET — безопасен для retry
      const response = await fetch(`/users/${userId}`);
      return response.json();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await sleep(1000 * (i + 1));
    }
  }
}

// Для кэширования
app.get('/products/:id', (req, res) => {
  // Можно безопасно кэшировать GET ответ
  res.set('Cache-Control', 'public, max-age=3600');
  res.json(product);
});

// Для параллельных запросов
Promise.all([
  fetch('/users/1'),
  fetch('/users/1'),
  fetch('/users/1'), // Безопасно повторять
]).then(([r1, r2, r3]) => {
  // Все результаты идентичны
});

GET-запрос всегда должен быть идемпотентным — это один из основных принципов REST архитектуры и HTTP протокола. Если GET запрос изменяет состояние, это нарушение стандарта и источник багов.