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

Всегда ли статус 200 означает успешное выполнение запроса

2.0 Middle🔥 191 комментариев
#HTML и CSS

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

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

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

Всегда ли статус 200 означает успешное выполнение запроса

Ответ: Нет, не всегда

Статус 200 OK — это только HTTP-уровневый ответ, который означает, что сервер успешно обработал запрос и отправил ответ. Однако это не гарантирует, что запрос выполнен корректно с бизнес-точки зрения. Это частая ошибка frontend разработчиков, которые доверяют только HTTP статусам.

HTTP статусы vs Бизнес-статусы

Что значит статус 200

HTTP 200 OK означает:
✅ Запрос был синтаксически корректен
✅ Сервер его обработал
✅ Ответ был успешно отправлен

❌ НЕ означает:
- Операция выполнена успешно (бизнес-логика)
- Данные актуальны и корректны
- Авторизация пройдена
- Никаких ошибок не было

Реальные примеры

// Пример 1: 200 с ошибкой в теле ответа
const response = await fetch('/api/v1/questions', {
  method: 'POST',
  body: JSON.stringify({ title: 'Question' })
});

if (response.status === 200) {
  const data = await response.json();
  // ❌ Ошибка! Не проверили data.success
  // Сервер может вернуть: { success: false, error: 'Invalid title' }
  console.log(data.id); // undefined!
}

// ✅ Правильно: проверяем и HTTP статус, и тело ответа
if (response.ok && response.status === 200) {
  const data = await response.json();
  if (data.success === true) {
    console.log(data.id);
  } else {
    console.error('Business error:', data.error);
  }
}

Пример 2: 200 с неполными данными

// ❌ Неправильно: доверяем только 200
const user = await fetch('/api/v1/users/me').then(r => r.json());
console.log(user.email); // undefined — сервер забыл поле!

// ✅ Правильно: валидируем структуру данных
interface User {
  id: string;
  email: string;
  name: string;
}

function validateUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'email' in data &&
    'name' in data &&
    typeof (data as Record<string, unknown>).email === 'string'
  );
}

const response = await fetch('/api/v1/users/me');
if (response.ok) {
  const data = await response.json();
  if (validateUser(data)) {
    console.log(data.email);
  } else {
    console.error('Invalid response structure');
  }
}

Различные сценарии HTTP 200

Сценарий 1: JSON с флагом ошибки

// API возвращает 200, но в теле сообщение об ошибке
const response = await fetch('/api/v1/login', {
  method: 'POST',
  body: JSON.stringify({ username: 'john', password: 'wrong' })
});

const data = await response.json();
// { "success": false, "error": "Invalid password" }

// ❌ Неправильно: только проверка статуса
if (response.status === 200) {
  localStorage.setItem('token', data.token); // token undefined!
}

// ✅ Правильно: проверяем бизнес-флаг
if (response.status === 200 && data.success === true) {
  localStorage.setItem('token', data.token);
} else {
  showError(data.error || 'Unknown error');
}

Сценарий 2: Пустой или невалидный JSON

// ❌ Неправильно
const response = await fetch('/api/v1/data');
const data = await response.json(); // Может бросить ошибку!
console.log(data.items); // undefined если пусто

// ✅ Правильно: обрабатываем ошибки парсинга
const response = await fetch('/api/v1/data');
if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

try {
  const data = await response.json();
  if (Array.isArray(data.items)) {
    return data.items;
  } else {
    throw new Error('Invalid response structure');
  }
} catch (err) {
  console.error('JSON parse error:', err);
  throw err;
}

Сценарий 3: 200 но с redirect

// ❌ Это может быть в старом коде, но не в fetch (он автоматически следует редиректам)
const response = await fetch('/api/v1/old-endpoint');
if (response.status === 200) {
  // fetch автоматически следует 301/302 редиректам
  // поэтому вы получите 200 с содержимым конечной страницы
  console.log(response.url); // Может отличаться от исходного URL
}

Правильная обработка HTTP запросов

interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
  code?: string;
}

interface Question {
  id: string;
  title: string;
  difficulty: 'easy' | 'medium' | 'hard';
}

async function fetchQuestion(id: string): Promise<Question> {
  // Этап 1: Проверяем HTTP статус
  const response = await fetch(`/api/v1/questions/${id}`);
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  // Этап 2: Парсим JSON
  let data: unknown;
  try {
    data = await response.json();
  } catch (err) {
    throw new Error('Failed to parse JSON response');
  }

  // Этап 3: Проверяем бизнес-уровневый успех
  if (typeof data !== 'object' || !data) {
    throw new Error('Invalid response structure');
  }

  const apiResponse = data as ApiResponse<Question>;
  
  if (!apiResponse.success) {
    throw new Error(`API Error: ${apiResponse.error || 'Unknown'}`);
  }

  // Этап 4: Валидируем данные
  const question = apiResponse.data;
  if (!question || typeof question !== 'object') {
    throw new Error('No data in response');
  }

  if (
    !('id' in question) ||
    !('title' in question) ||
    !('difficulty' in question)
  ) {
    throw new Error('Question missing required fields');
  }

  return question as Question;
}

// Использование
try {
  const question = await fetchQuestion('123');
  console.log('Success:', question);
} catch (err) {
  console.error('Failed to fetch question:', err);
  // Показываем пользователю понятное сообщение об ошибке
}

HTTP статусы и их реальное значение

2xx — Успешно обработано
  200 OK — запрос обработан, но проверьте тело ответа!
  201 Created — ресурс создан
  204 No Content — успешно, но нет данных
  206 Partial Content — частичное содержимое (диапазоны байт)

3xx — Редирект
  301 Moved Permanently — перемещено (fetch следует автоматически)
  304 Not Modified — кэшированные данные актуальны

4xx — Ошибка клиента
  400 Bad Request — неправильный формат
  401 Unauthorized — нужна авторизация
  403 Forbidden — доступ запрещён
  404 Not Found — не найдено
  429 Too Many Requests — лимит запросов

5xx — Ошибка сервера
  500 Internal Server Error — внутренняя ошибка
  503 Service Unavailable — сервис недоступен

Лучшие практики

// ✅ Создай хелпер для API запросов
interface ApiOptions extends RequestInit {
  timeout?: number;
}

async function apiCall<T>(
  url: string,
  options: ApiOptions = {}
): Promise<T> {
  const { timeout = 5000, ...fetchOptions } = options;

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...fetchOptions,
      signal: controller.signal
    });

    // Проверяй HTTP статус
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    // Парсь JSON с обработкой ошибок
    const data = await response.json();

    // Проверяй структуру (используй Zod, Yup или io-ts)
    return data as T;
  } finally {
    clearTimeout(timeoutId);
  }
}

// ✅ Используй Zod для валидации
import { z } from 'zod';

const QuestionSchema = z.object({
  id: z.string().uuid(),
  title: z.string().min(1),
  difficulty: z.enum(['easy', 'medium', 'hard'])
});

type Question = z.infer<typeof QuestionSchema>;

const data = await apiCall('/api/v1/questions/123');
const validated = QuestionSchema.parse(data); // Выбросит ошибку если невалидно

Заключение

Статус 200 — это только начало. Правильная обработка API запросов требует:

  1. Проверка HTTP статуса (response.ok или status >= 200 && status < 300)
  2. Обработка ошибок парсинга (JSON может быть невалидным)
  3. Проверка бизнес-уровня (может быть флаг success, error code и т.д.)
  4. Валидация данных (Zod, TypeGuards, или ручная проверка)
  5. Обработка таймаутов и сетевых ошибок

Только полная цепочка проверок гарантирует, что запрос действительно выполнен успешно.