Перейти к содержанию

192 уроков, 14 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 3 из 10 Средний 40 мин 120 XP

Коды ответа 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 не нужно. Модуль исполняется прямо здесь:

http.HTTPStatus: имя для каждого кода
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 делает внутри, когда решает, считать ли ответ удачным. Таблица ниже — переводчик, который стоит держать под рукой:

КодНазваниеЧто произошло
200OKОбычный успех: данные в теле ответа
201CreatedУспех плюс: создан новый ресурс (ответ на POST)
301Moved PermanentlyРесурс навсегда переехал на новый адрес
403ForbiddenРесурс есть, но доступ запрещён: не твой токен, не твой IP
404Not FoundТакого ресурса нет: опечатка в адресе или страницу удалили
429Too Many RequestsТы стучишься слишком часто: притормози (rate limit)
500Internal Server ErrorОшибка в коде сервера — ты тут ни при чём
503Service UnavailableСервер перегружен или на обслуживании — попробуй позже

Пара комментариев к списку. 403 и 404 различаются принципиально: 403 честно признаёт «страница есть, но не пущу», 404 говорит «страницы нет» — часто её ставят и там, где прятать нечего, но не хотят раскрывать структуру сайта. А 429 — любимый ответ API, когда скрипт без пауз штампует запросы; в уроках про пагинацию и вежливый парсинг это главный стоп-сигнал. Рядом с 403 живёт и 401 Unauthorized — «не представился». Разница для практики: 401 намекает передать учётные данные (токен, логин-пароль) и повторить запрос, а 403 после правильной авторизации означает «всё верно, но нельзя» — повторять бессмысленно. Оба кода мы ещё встретим в уроке про авторизацию токенами.

Семейство подсказывает и стратегию повтора. 5xx — часто временная беда: сервер перегружен, идёт деплой; такой запрос можно повторить через пару секунд, постепенно увеличивая задержку. 4xx повторять бессмысленно: пока не поменяешь сам запрос — адрес, параметры, токен, — сервер ответит тем же. Единственное исключение — 429: он просит не «чинить», а замедлиться.

Свяжем с прошлым уроком: код живёт в первой строке ответа — HTTP/1.1 503 Service Unavailable. Достанем его из строки и вынесем вердикт, как это делает requests:

читаем код из статус-строки урока 1
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 — у него это свойство реализовано точно так же:

ok: код меньше 400 - хорошо
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 сторож спит:

механика raise_for_status своими руками
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 проверка живёт в одном месте: вызвал сразу после запроса — и дальше код просто не выполняется, если ответ плохой, а исключение ловится там, где у тебя есть план Б.

реальный запрос с 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())        # до этой строки дело не дойдёт
Сеть в браузерном интерпретаторе недоступна — этот код запускается на твоём компьютере после pip install requests. Настоящий HTTPError выглядит так: 404 Client Error: Not Found for url: ... — в сообщении код, фраза и полный адрес.
ловим HTTPError и достаём код из исключения
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)
Снова код для твоего компьютера: у объекта HTTPError есть поле response, и из него достаётся код, статус и даже тело ответа сервера. В песочнице та же механика исполняется в блоке выше на каркасе FakeResponse.

Куда деваются 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(" ", "_"))
Проверь себя
0 / 5

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?

Карточки терминов
Запомнено: 0 / 6
Практика

Тренируем сторожа: в редакторе класс FakeResponse с методом raise_for_status и список из трёх ответов — 200, 404 и 503. Выведи для каждой страницы код и ok (через пробел), затем поймай HTTPError от pages[1] и выведи «Поймали:» с текстом исключения, а в конце вызови raise_for_status() у pages[0] и выведи «200 - тихо».

practice.py
Вопросы и ответы по уроку

Почему 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.