Learning Platform
Глоссарий Troubleshooting
Урок 03.04 · 20 мин
Начальный
HTTPStatus codesErrorsREST

Статус-коды — все 5 классов и ключевые коды

Статус-код — это первое, что смотрит ваш клиент в HTTP-ответе. Три цифры, и ваш код решает: радоваться, ругаться или ретраить. На бумаге кажется простым, на деле в разнице между 401 и 403, 502 и 504, 422 и 400 — годы опыта инженеров. В этом уроке разберём все коды, с которыми вы столкнётесь как Junior DE, и научимся правильно реагировать на каждый.


Структура: первая цифра определяет класс

Все статус-коды трёхзначные и делятся на 5 классов по первой цифре:

Пять классов HTTP-статус-кодов
1xxИнформационные. Промежуточные ответы. Junior встретит редко: только 100 Continue и 101 Switching Protocols (WebSocket upgrade)
2xxУспех. Запрос обработан. 200 OK, 201 Created, 204 No Content -- основные
3xxПеренаправление. Ресурс находится в другом месте. 301, 302, 304, 307 -- главные
4xxОшибка клиента. Что-то не так с запросом: невалидные данные, нет прав, не найден ресурс. Сервер не виноват
5xxОшибка сервера. Запрос валидный, но сервер не справился. Junior шлёт правильный запрос -- но получает 500

Простое правило: 4xx — твоя проблема, чини запрос. 5xx — их проблема, повтори позже. Это упрощение, но как первый ориентир работает хорошо.


1xx Informational — промежуточные

Эти коды — промежуточные ответы. Сервер говорит «я ещё не закончил, подожди». Junior встречает редко.

  • 100 Continue — сервер готов принять тело запроса. Используется при больших uploads: клиент шлёт заголовки с Expect: 100-continue, ждёт 100, потом шлёт тело. Если сервер не готов (например, тело слишком большое) — он ответит сразу 413, не приняв тело.
  • 101 Switching Protocols — сервер согласен переключиться на другой протокол. Используется при WebSocket-handshake (модуль 11): клиент шлёт Upgrade: websocket, сервер отвечает 101 и переходит на WebSocket-протокол.
  • 102 Processing, 103 Early Hints — экзотика, в ETL вам не нужны.

2xx Success — всё хорошо

Самый приятный класс. Запрос обработан успешно.

Главные 2xx коды
200 OKУниверсальный успех. Используется для GET, успешных POST/PUT/PATCH с возвратом ресурса. Тело ответа есть и содержит данные
201 CreatedРесурс создан. Часто после POST (создание новой записи). По REST-конвенции должен быть Location-заголовок с URL нового ресурса
202 AcceptedЗапрос принят, обработка асинхронная. Сервер обещает обработать, но позже. Клиент должен polling или ждать webhook. Типично для long-running jobs
204 No ContentУспех, но без тела ответа. Часто после DELETE или PUT, когда отдельный ответ не нужен
206 Partial ContentСервер вернул кусок ресурса (по Range-заголовку). Используется в video streaming, resumable downloads

Для Junior DE главные:

  • 200 — стандартный успех на GET и большинство POST.
  • 201 — создание (стандарт REST).
  • 204 — успех без тела (чаще на DELETE).
  • 202 — асинхронная обработка. Если получили — храните job ID и polling-те /jobs/{id}.
# Пример 201 с Location:
curl -i -X POST https://httpbin.org/post -d '{"name":"test"}'
# HTTP/1.1 200 OK   <- httpbin возвращает 200, в реальном API было бы 201

# Пример 204 -- DELETE без тела:
curl -i -X DELETE https://httpbin.org/delete
# HTTP/1.1 200 OK   <- опять же httpbin не знает про 204

3xx Redirection — перенаправление

Сервер говорит «иди в другое место». Браузеры обычно следуют автоматически. HTTP-клиенты в коде — настраиваемо.

Главные 3xx коды
301 Moved PermanentlyРесурс навсегда переехал. Клиент должен запомнить новый URL и больше не ходить на старый. Браузеры кэшируют этот редирект
302 FoundВременное перенаправление. Старый URL ещё актуален, клиент должен и впредь по нему ходить, но сейчас идти по Location
304 Not ModifiedОтвет к conditional GET (с If-Modified-Since или If-None-Match). 'Ресурс не изменился, используй кэш'. БЕЗ ТЕЛА -- экономия трафика
307 Temporary RedirectКак 302, но строгий: метод НЕ меняется. POST + 307 = клиент должен повторить тот же POST на новый URL
308 Permanent RedirectКак 301, но строгий: метод НЕ меняется. POST + 308 = повторить POST на новый URL

Тонкий момент: 301 и 302 vs 307 и 308. Исторически многие клиенты при 301/302 в ответ на POST меняли метод на GET (это не по спеке, но так делали). 307 и 308 (HTTP/1.1, RFC 7538) добавлены, чтобы явно требовать сохранения метода. На практике:

  • GET + 301/302 — клиент идёт по новому URL с GET. Без проблем.
  • POST + 301/302 — поведение зависит от клиента! Curl, requests, httpx по умолчанию меняют POST на GET (согласно историческому поведению). Это может быть неожиданно.
  • POST + 307/308 — все клиенты повторяют POST.
# Посмотреть редирект, не следуя:
curl -I https://www.github.com
# HTTP/1.1 301 Moved Permanently
# Location: https://github.com/

# Следовать редиректам:
curl -L https://www.github.com
# Редиректит до конечного URL и возвращает финальный ответ

304 Not Modified — отдельная история. Сервер отвечает им только на conditional GET — когда клиент послал If-Modified-Since или If-None-Match с ETag. Если ресурс не изменился, сервер не шлёт тело — экономия трафика. Подробно про кэширование — модуль 3.

WARNING

В Python requests по умолчанию следует редиректам (allow_redirects=True). Это удобно, но опасно: если API делает POST -> 302 -> другой POST, requests может сделать GET вместо POST на втором шаге. Если API критичен, лучше явно ставить allow_redirects=False и обрабатывать редиректы руками.


4xx Client Error — твоя проблема

Самый интересный класс. Сервер говорит: «то, что ты прислал, не валидно». Здесь Junior встречает большинство загадочных ситуаций.

Главные 4xx коды
400 Bad RequestЗапрос невалидный на уровне HTTP или базовых полей. Битый JSON, неправильные типы. Сервер не смог даже распарсить запрос
401 UnauthorizedКто ты? Не аутентифицирован. Нет Authorization-заголовка или токен невалидный/истёк
403 ForbiddenЗнаю, кто ты. Но у тебя нет прав на эту операцию. Аутентифицирован, но не авторизован
404 Not FoundЗапрошенного ресурса не существует. /users/99999 -- нет такого юзера
405 Method Not AllowedМетод не поддерживается на этом endpoint. POST на /users/42 -- может быть только GET/PUT/DELETE
409 ConflictКонфликт состояния. Например, версионирование: вы пытаетесь обновить старую версию документа
410 GoneРесурс был, но удалён НАВСЕГДА. В отличие от 404, это явное сообщение 'не ищи, не вернётся'
422 Unprocessable EntityЗапрос валиден на уровне HTTP/JSON, но семантически неверный. Например, age=-5. Pydantic-валидация типично возвращает 422
429 Too Many RequestsRate limit. Слишком много запросов за период. Должен быть Retry-After заголовок (секунды или дата)

401 vs 403 — ключевая разница

Эта пара путает Junior чаще всего. Запомните:

  • 401 Unauthorized = «Я не знаю, кто ты». Нет токена или токен невалидный/истёк. Решение клиента: обновить токен или залогиниться.
  • 403 Forbidden = «Я знаю, кто ты, но тебе сюда нельзя». Токен валидный, но недостаточно прав. Решение клиента: жаловаться админу, новый токен не поможет.

Если на ваш Authorization: Bearer xyz API стабильно отвечает 403 — не пытайтесь обновлять токен, это не поможет. У этой роли нет доступа к этому endpoint.

# 401: невалидный токен
curl -i -H 'Authorization: Bearer invalid_token' https://api.github.com/user
# HTTP/2 401
# {"message":"Bad credentials","documentation_url":"..."}

# 403: валидный токен, но нет прав на конкретный ресурс (например, private repo чужого юзера)
# 403 от GitHub также используется для rate-limit без токена -- это неклассически, но так делают

422 vs 400 — структурная vs семантическая невалидность

  • 400 Bad Request — запрос невозможно даже распарсить. Битый JSON, неправильный Content-Type, отсутствует обязательное поле в headers.
  • 422 Unprocessable Entity — JSON валидный, поля есть, но значения семантически неверные. age=-5, email без @, дата в прошлом, когда ожидается будущее.

Стандарт REST: используйте 422 для валидации полей. На практике многие API возвращают 400 для всех валидационных ошибок — тоже допустимо. FastAPI/Pydantic возвращают 422 по умолчанию.

429 Too Many Requests — главный враг ETL

Retries и rate limits: tenacity на практике

429 — самый болезненный код для Junior DE. Почти каждый production API имеет rate limits, и почти каждый ETL-скрипт рано или поздно в них упирается.

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1715692800

{"message":"API rate limit exceeded"}

Что делать: прочитать Retry-After и подождать. Заголовок может быть в двух форматах:

  • Секунды: Retry-After: 60 — подожди 60 секунд.
  • HTTP-date: Retry-After: Wed, 21 Oct 2025 07:28:00 GMT — подожди до этого момента.

Ни в коем случае не «ретраить сразу же» — это типичный антипаттерн, который ведёт к ban’у IP. Подробно про rate limits и стратегии — модуль 9.

WARNING

Если ETL получает 429 без Retry-After — это плохой API. Используйте exponential backoff: 1с, 2с, 4с, 8с, 16с, потом отказывайтесь. Не превращайтесь в DDoS-бот.


5xx Server Error — их проблема

Зачем нужен load balancer: scalability, redundancy, health checks

Сервер сломался. Запрос валидный, но обработать не получилось.

Главные 5xx коды
500 Internal Server ErrorУниверсальная серверная ошибка. Что-то упало в коде сервера. Никакой полезной информации для клиента -- нужно смотреть логи сервера
502 Bad GatewayПрокси/балансировщик получил невалидный ответ от upstream-сервера. Часто backend упал, а балансировщик до сих пор пытается ходить
503 Service UnavailableСервер временно недоступен (перегрузка, maintenance). Должен быть Retry-After. В отличие от 502 -- сервер сам говорит 'я не могу прямо сейчас', а не балансировщик гадает
504 Gateway TimeoutПрокси/балансировщик не дождался ответа от upstream. Запрос висит дольше, чем готов ждать прокси (обычно 30-60 секунд)

Когда какой код?

  • 500 — есть, но мало информации. Что-то упало внутри. Иногда возвращают, когда не знают, какой код использовать. Нужно смотреть X-Request-Id (если есть) и идти к owner API за помощью.
  • 502 — балансировщик/CDN не может связаться с реальным сервером. Backend упал, сеть моргнула. Обычно временно.
  • 503 — сервер сам сказал «я перегружен». Должен быть Retry-After. Можно ждать и retry.
  • 504 — таймаут на проксе. Backend думает дольше, чем балансировщик готов ждать. Часто признак того, что ваш запрос слишком тяжёлый — попробуйте уменьшить limit или page size.

Стратегия retry для 5xx

5xx обычно ретраиться имеет смысл — ошибка временная. Но:

  1. GET, HEAD, PUT, DELETE — безопасно ретраить (idempotent).
  2. POST, PATCH — ретраить только с Idempotency-Key, иначе дубликаты.
  3. Exponential backoff — 1с, 2с, 4с, 8с, 16с. Не больше 5 попыток. Подробнее — модуль 9.
import requests
import time

def get_with_retry(url, max_attempts=5):
    for attempt in range(max_attempts):
        try:
            response = requests.get(url, timeout=10)
            if response.status_code in (500, 502, 503, 504):
                wait = 2 ** attempt
                print(f"Got {response.status_code}, waiting {wait}s")
                time.sleep(wait)
                continue
            return response
        except requests.Timeout:
            wait = 2 ** attempt
            time.sleep(wait)
    raise Exception(f"Failed after {max_attempts} attempts")

Стратегия обработки кодов в коде

Как Junior должен думать про статус-коды? Простая ментальная модель:

Решающее дерево обработки кодов
2xxУспех. response.json() или response.text -- что вы там ожидаете
OKПарсинг и продолжение работы
3xxОбычно не доходит до вашего кода -- клиент сам обработал. Если выключили follow_redirects -- обработайте Location руками
Follow or save LocationПерейти по Location или сохранить новый URL для будущих запросов
4xxЧто-то не так с запросом. НЕ ретраить (кроме 408 Timeout, 429 Rate Limit, 423 Locked)
Logging + жалоба или 429-retry401 -- обновить токен. 403 -- нет прав, жаловаться. 404 -- ресурса нет, OK. 422 -- невалидные данные, починить запрос. 429 -- подождать Retry-After
5xxСервер сломался. РЕТРАИТЬ с exponential backoff. POST -- только с Idempotency-Key
Retry с backoffПодождать, попробовать ещё раз. После N неудач -- alarm в monitoring

В Python с requests/httpx есть удобный метод response.raise_for_status() — кидает исключение для 4xx и 5xx. Используйте его, когда хотите fail-fast:

response = requests.get('https://api.github.com/users/torvalds')
response.raise_for_status()  # кинет HTTPError для 4xx/5xx
data = response.json()

Попробуй сам

Поэкспериментируйте с кодами на httpbin:

# Получить любой статус-код:
curl -i https://httpbin.org/status/200
curl -i https://httpbin.org/status/404
curl -i https://httpbin.org/status/500
curl -i https://httpbin.org/status/429

# 401 без аутентификации:
curl -i https://httpbin.org/basic-auth/user/passwd
# HTTP/1.1 401 Unauthorized

# 401 с правильным паролем -> 200:
curl -i -u user:passwd https://httpbin.org/basic-auth/user/passwd

# Симулировать редирект:
curl -i https://httpbin.org/redirect/3
# Без -L: 302 Found, Location: /redirect/2
curl -L https://httpbin.org/redirect/3
# С -L: пройдёт все 3 редиректа

# Симулировать задержку (timeout):
curl -m 2 https://httpbin.org/delay/5
# Закроет соединение через 2 секунды (-m 2)

Поиграйтесь, посмотрите, как разные клиенты реагируют. Через 15 минут такой практики вы будете чувствовать статус-коды интуитивно.


Проверка знанийKnowledge check
Junior получает от API ответ '403 Forbidden' и думает: 'Наверное, токен истёк, надо обновить и попробовать снова'. Прав ли он? И как должна выглядеть правильная реакция?
ОтветAnswer
Junior НЕ прав. 403 Forbidden означает 'я знаю, кто ты (токен валидный), но у тебя нет прав на эту операцию'. Если бы токен истёк или был невалидный, сервер вернул бы 401 Unauthorized ('я не знаю, кто ты'). Правильная реакция при 403: НЕ обновлять токен (это не поможет), а проверить: (1) правильный ли user/role используется -- может, для этого endpoint нужны admin-права, а у вас reader; (2) правильный ли scope в OAuth2-токене -- может, токен валидный, но не запрашивал нужных прав; (3) идти к owner API и просить расширения прав или другого аккаунта. Если обновлять токен на каждом 403 -- будете крутиться в бесконечном цикле, потому что новый токен с теми же правами тоже получит 403. Это типичная ошибка, которая ест время в дебаге часами. Запоминание: 401 = WHO are you?, 403 = I know you, but NO. И ещё нюанс: некоторые API (включая GitHub) возвращают 403 для rate limit -- это исторический баг семантики, в идеале должно быть 429. Поэтому при 403 всегда смотрите тело ответа и заголовки X-RateLimit-* -- если там что-то про rate limit, это 429-в-маске.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. В чём ключевая семантическая разница между 401 Unauthorized и 403 Forbidden?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5