Коды ответа HTTP: status_code, ok и raise_for_status в requests
Семейства кодов ответа, свойство ok и метод raise_for_status: узнаём, что сервер на самом деле сказал, и ловим HTTPError — механику проверяем прямо на странице.
Редакция Питоники
В первом уроке сервер вернул тебе 200 OK, и всё было хорошо. Но настоящие серверы отвечают и по-другому: страница переехала, доступ закрыт, сервер перегружен, ты опечатался в адресе. Все эти новости сервер сообщает кодом ответа — тремя цифрами в первой строке. Сегодня научимся их читать, сортировать по семействам и заставим requests громко падать, когда ответ плохой.
Инструменты урока — снова без сети: встроенный модуль http.HTTPStatus и каркас FakeResponse из первого урока. Всё исполняется на странице, а реальный код requests пойдёт рядом — для твоего компьютера.
Что значит код ответа: четыре семейства
Код — это три цифры, и смотреть нужно на первую. Она задаёт класс ответа, и классов ровно четыре, которые встречаются в работе: 2xx — успех («всё получил, вот данные»), 3xx — перенаправление («это тут, но теперь живёт там»), 4xx — ошибка клиента («ты что-то не то попросил»), 5xx — ошибка сервера («я понял, но сломался»). Запомни это деление — оно мгновенно подсказывает, чья сторона виновата и что чинить.
В Python у каждого кода есть имя, и живёт оно в стандартном модуле http.HTTPStatus — никакого requests не нужно. Модуль исполняется прямо здесь:
from http import HTTPStatus
print(HTTPStatus.OK.value, HTTPStatus.OK.phrase)
print(HTTPStatus.NOT_FOUND.value, HTTPStatus.NOT_FOUND.phrase)
print(HTTPStatus(404).phrase)
print(HTTPStatus(500).phrase)
200 OK 404 Not Found Not Found Internal Server Error
Две формы обращения: HTTPStatus.NOT_FOUND — когда хочешь читаемое имя, и HTTPStatus(404) — когда код пришёл из ответа и его надо расшифровать. У каждой записи есть .value (число) и .phrase (официальная фраза). А теперь рассортируем весь «рабочий набор» кодов по семействам:
from http import HTTPStatus
for code in (200, 201, 301, 403, 404, 429, 500, 503):
family = code // 100 * 100
print(code, HTTPStatus(code).phrase, "- семейство", family)
200 OK - семейство 200 201 Created - семейство 200 301 Moved Permanently - семейство 300 403 Forbidden - семейство 400 404 Not Found - семейство 400 429 Too Many Requests - семейство 400 500 Internal Server Error - семейство 500 503 Service Unavailable - семейство 500
Целочисленное деление code // 100 * 100 вычисляет семейство из самого числа — то же самое, что requests делает внутри, когда решает, считать ли ответ удачным. Таблица ниже — переводчик, который стоит держать под рукой:
| Код | Название | Что произошло |
|---|---|---|
| 200 | OK | Обычный успех: данные в теле ответа |
| 201 | Created | Успех плюс: создан новый ресурс (ответ на POST) |
| 301 | Moved Permanently | Ресурс навсегда переехал на новый адрес |
| 403 | Forbidden | Ресурс есть, но доступ запрещён: не твой токен, не твой IP |
| 404 | Not Found | Такого ресурса нет: опечатка в адресе или страницу удалили |
| 429 | Too Many Requests | Ты стучишься слишком часто: притормози (rate limit) |
| 500 | Internal Server Error | Ошибка в коде сервера — ты тут ни при чём |
| 503 | Service Unavailable | Сервер перегружен или на обслуживании — попробуй позже |
Пара комментариев к списку. 403 и 404 различаются принципиально: 403 честно признаёт «страница есть, но не пущу», 404 говорит «страницы нет» — часто её ставят и там, где прятать нечего, но не хотят раскрывать структуру сайта. А 429 — любимый ответ API, когда скрипт без пауз штампует запросы; в уроках про пагинацию и вежливый парсинг это главный стоп-сигнал. Рядом с 403 живёт и 401 Unauthorized — «не представился». Разница для практики: 401 намекает передать учётные данные (токен, логин-пароль) и повторить запрос, а 403 после правильной авторизации означает «всё верно, но нельзя» — повторять бессмысленно. Оба кода мы ещё встретим в уроке про авторизацию токенами.
Семейство подсказывает и стратегию повтора. 5xx — часто временная беда: сервер перегружен, идёт деплой; такой запрос можно повторить через пару секунд, постепенно увеличивая задержку. 4xx повторять бессмысленно: пока не поменяешь сам запрос — адрес, параметры, токен, — сервер ответит тем же. Единственное исключение — 429: он просит не «чинить», а замедлиться.
Свяжем с прошлым уроком: код живёт в первой строке ответа — HTTP/1.1 503 Service Unavailable. Достанем его из строки и вынесем вердикт, как это делает requests:
line = "HTTP/1.1 503 Service Unavailable"
code = int(line.split()[1])
family = code // 100
if family == 2:
verdict = "успех"
elif family == 3:
verdict = "редирект"
elif family == 4:
verdict = "ошибка клиента"
else:
verdict = "ошибка сервера"
print(code, "->", verdict)
503 -> ошибка сервера
Что делает свойство response.ok?
В requests у каждого ответа есть булево свойство ok. Его формула обидно проста: status_code < 400. Всё, что меньше четырёхсот — включая редиректы 3xx, — считается «хорошо»; четвёрки и пятёрки — «плохо». Проверим на каркасе FakeResponse — у него это свойство реализовано точно так же:
class FakeResponse:
def __init__(self, status_code, text, headers=None):
self.status_code = status_code
self.text = text
self.headers = headers or {}
def json(self):
import json
return json.loads(self.text)
@property
def ok(self):
return self.status_code < 400
answers = [
FakeResponse(200, '{"ok": true}'),
FakeResponse(301, ""),
FakeResponse(404, "нет такой страницы"),
]
for r in answers:
print(r.status_code, "ok:", r.ok)
print("404 не уронил программу:", answers[2].text)
200 ok: True 301 ok: True 404 ok: False 404 не уронил программу: нет такой страницы
Самая важная строка в выводе — последняя: ответ с кодом 404 не уронил программу. Цикл спокойно дошёл до конца. Это не баг каркаса — настоящий requests ведёт себя точно так же.
Почему 404 — это не ошибка Python?
Потому что для Python всё прошло штатно: функция requests.get своё дело сделала — дошла до сервера, получила ответ, вернула объект. Сервер тоже сделал своё дело — честно сообщил: страницы нет. Код 404 — это не сбой программы, а содержание ответа. Ошибкой Python было бы другое: недоступен сам сервер (обрыв сети, DNS) — тогда requests бросит ConnectionError.
Код 404 — это не ошибка Python: сервер честно ответил, просто страницы нет. Проверить ответ — твоя работа. Три способа на выбор: сравнить response.status_code == 200, посмотреть if response.ok или заставить библиотеку кричать — вызвать response.raise_for_status(). Дальше — о третьем способе, самом громком. Быстрая практика на каждый день: три строки if not response.ok: -> печать кода и выход — спасают любой сборщик данных. А для API-клиента удобнее завести свою функцию-сторожа, которая внутри зовёт raise_for_status(), и звать её после каждого запроса: дальше по коду можно смело верить, что тело — валидное. Две строки профилактики всегда дешевле вечера дебага.
Что делает raise_for_status?
Метод raise_for_status() — это сторож: он смотрит на код, и если тот 4xx или 5xx, бросает исключение HTTPError с кодом и адресом в тексте. Если код хороший — молча возвращает тот же ответ, ничего не меняя. Напишем такую проверку сами, слово в слово как в requests, — и убедимся, что на 200 сторож спит:
class HTTPError(Exception):
"""Исключение, которое бросает raise_for_status при коде >= 400."""
class FakeResponse:
def __init__(self, status_code, text):
self.status_code = status_code
self.text = text
@property
def ok(self):
return self.status_code < 400
def raise_for_status(self):
if self.status_code >= 400:
raise HTTPError(str(self.status_code) + " Error: " + self.text)
return self
r_ok = FakeResponse(200, '{"temp": 12}')
r_404 = FakeResponse(404, "page not found")
r_ok.raise_for_status()
print("200 прошёл молча")
try:
r_404.raise_for_status()
except HTTPError as e:
print("Поймали HTTPError:", e)
200 прошёл молча Поймали HTTPError: 404 Error: page not found
Смотри, как меняется стиль программы. Без raise_for_status ты обязан помнить о проверке в каждом месте: забыл одну ветку if — и скрипт молча обработал страницу-ошибку как данные. С raise_for_status проверка живёт в одном месте: вызвал сразу после запроса — и дальше код просто не выполняется, если ответ плохой, а исключение ловится там, где у тебя есть план Б.
import requests
response = requests.get(
"https://api.pythonika.ru/weather/Moon", timeout=5
)
response.raise_for_status() # здесь вылетит HTTPError: 404
print(response.json()) # до этой строки дело не дойдёт
import requests
from requests.exceptions import HTTPError
try:
response = requests.get(
"https://api.pythonika.ru/weather/Moon", timeout=5
)
response.raise_for_status()
data = response.json()
except HTTPError as e:
print("Сервер вернул ошибку:", e.response.status_code)
Куда деваются 301 редиректы?
Остался недосказанный кадр: ответ 301 Moved Permanently. Сам по себе он бесполезен — в нём спрятан новый адрес, заголовок Location. Хороший клиент должен пойти по нему. requests делает это автоматически и молча, поэтому ты почти никогда не видишь 301 в response.status_code — вместо этого видишь финальный код:
class FakeResponse:
def __init__(self, status_code, text, headers=None):
self.status_code = status_code
self.text = text
self.headers = headers or {}
history = [
FakeResponse(301, "", {"Location": "https://pythonika.ru/http"}),
FakeResponse(200, "Страница на новом адресе"),
]
print("Первый ответ:", history[0].status_code)
print("Новый адрес:", history[0].headers["Location"])
print("Финальный код:", history[-1].status_code)
Первый ответ: 301 Новый адрес: https://pythonika.ru/http Финальный код: 200
Промежуточные ответы библиотека складывает в response.history, а в response.status_code остаётся финальный 200. Отсюда и польза raise_for_status: он оценивает уже итоговый ответ, после всех прыжков. Если однажды понадобится запретить автоматические переходы — у requests есть флаг allow_redirects=False: тогда 301 останется в status_code, а новый адрес придётся прочитать из заголовка Location самому.
Что дальше
Ты собрал полный словарь общения с сервером: коды делятся на четыре семейства, status_code держит число, ok — быстрый булев фильтр, raise_for_status — громкая сирена с HTTPError в руках. Редиректы requests проходит сам, а 429 теперь не сюрприз, а команда сбавить темп.
Следующий вопрос — что именно лежит в теле: три лица ответа text, content и json — про строки, байты и кодировки. А применить сегодня полученное можно в первом парсере на requests и BeautifulSoup: там проверка status_code перед разбором HTML — обязательный шаг, иначе парсер начнёт «разбирать» страницу-ошибку. Скучная проверка на 404 экономит часы дебага — проверено.
Хороший клиент отличается от плохого не тем, что не попадает в ошибки, а тем, что узнаёт о них мгновенно.
Сначала предскажи ответ в голове — это главный навык программиста.
def ok(code):
return code < 400
codes = [200, 301, 403, 404, 500]
print(sum(1 for c in codes if not ok(c)))
line = "HTTP/1.1 503 Service Unavailable"
print(line.split()[1])
print(int(line.split()[1]) // 100)
from http import HTTPStatus
print(HTTPStatus(429).phrase.lower().replace(" ", "_"))
1. Сервер вернул код 429. Чья это проблема и что он означает?
2. Что вернёт response.ok при коде ответа 500?
3. Что произойдёт при вызове response.raise_for_status(), если код ответа 200?
4. Сервер ответил 301 Moved Permanently. Что вернёт response.status_code у requests?
5. Какое исключение бросает raise_for_status при коде 404?
Тренируем сторожа: в редакторе класс FakeResponse с методом raise_for_status и список из трёх ответов — 200, 404 и 503. Выведи для каждой страницы код и ok (через пробел), затем поймай HTTPError от pages[1] и выведи «Поймали:» с текстом исключения, а в конце вызови raise_for_status() у pages[0] и выведи «200 - тихо».
Почему requests не бросает исключение при ошибке 404?
Потому что 404 — это не сбой, а честный ответ сервера: запрос дошёл, сервер ответил «страницы нет». requests вернёт обычный объект Response с status_code 404. Исключения в requests — только про транспорт: обрыв сети (ConnectionError) или таймаут. Чтобы ошибка ответа становилась исключением, вызывай response.raise_for_status() — он бросит HTTPError.
Чем ok отличается от raise_for_status?
ok — вопрос без последствий: булево свойство «код меньше 400», удобно в if. raise_for_status — действие: при коде 4xx/5xx бросает исключение HTTPError и обрывает выполнение до ближайшего except. Первое — для тихих проверок и ветвлений, второе — когда без валидного ответа работать дальше бессмысленно.
Чем 403 отличается от 404?
403 Forbidden означает «ресурс существует, но доступ запрещён» — не тот токен, закрытый раздел, блокировка IP. 404 Not Found — «такого ресурса нет». Многие сайты намеренно отдают 404 вместо 403 на закрытых страницах, чтобы не раскрывать структуру каталогов.
Что делать при ошибке 429 Too Many Requests?
Притормозить: 429 — команда rate limit, ты шлёшь запросы быстрее, чем сервер разрешает. Добавь паузы time.sleep между запросами, повторы с растущей задержкой (backoff) и уважай заголовок Retry-After, если сервер его присылает. Схема повторов подробно разбирается в уроке курса про таймауты и повторные попытки.
Понравился урок? Сошлитесь на него
«Код 404 — это не ошибка Python: сервер честно ответил, просто страницы нет. Проверить ответ — твоя работа.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
requests · Урок 2
Параметры GET-запроса: params и query string в requests
Query string глазами Python: словарь params превращается в ?city=Moscow&days=3, русские буквы — в проценты, а parse_qs разворачивает всё обратно. Каждый шаг исполняется на странице.
BeautifulSoup / Scrapy · Урок 2
Первый парсер: requests и BeautifulSoup — скачиваем и разбираем страницу
Собираем первого рабочего парсера по схеме «скачать — разобрать — достать»: requests скачивает страницу, BeautifulSoup превращает её в объект, из которого данные достаются в одну строку.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
FastAPI · Урок 6
Ответы сервера: response_model, статусы и заголовки
response_model фильтрует поля ответа, HTTPException отдаёт внятные ошибки, заголовок Location ведёт к созданному ресурсу — договор сервера с клиентом.
обработка ошибок fastapifastapi status_code
json · Урок 14
json.loads на ответе API: от текста к данным
Текст, json.loads, словарь, поля: собираем полный конвейер разбора ответа API — с проверкой структуры, обработкой пустого и битого тела.
response.json() pythonpython получить данные из ответа сервера
requests · Урок 1
Библиотека requests Python с нуля: первый GET-запрос
Первый GET-запрос на requests: что улетает по сети, что возвращается и как разобрать ответ на статус, заголовки и тело — механику проверяем прямо в браузере.
requests pythonбиблиотека requests