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

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

Начать обучение
Урок 11 из 20 Средний 35 мин 120 XP

Обработка ошибок во Flask: свои страницы 404 и 500

Одиннадцатый урок курса Flask: гость опечатался в адресе — и видит английскую заглушку без вашего меню. Учимся отдавать свои страницы 404 и 500 через @app.errorhandler, честно отвечать abort(404), когда записи нет, и проверять статусы через test_client.

Редакция Питоники

Блог из урока 10 готов и работает. Но вот гость переслал другу ссылку с опечаткой: /post/7 превратилась в /postt/7. Что тот увидит? Английскую страницу «404 Not Found» на белом фоне — без меню, без стилей, без намёка, как вернуться на сайт. Сайт будто исчез и заменился системной заглушкой. То же случится, если ссылка на старую статью осталась в чужом блоге, а вы её удалили.

Во втором сценарии хуже: где-то в коде случилось исключение, и гость получил «500 Internal Server Error» — опять чужую заглушку, хотя сайт-то ваш. Сегодня вы научитесь отдавать на оба случая собственные страницы: зарегистрируете обработчики @app.errorhandler, познакомитесь с abort и заодно привыкнете проверять статусы ответов через test_client — приём из урока 2, который здесь раскрывается в полную силу.

Что гость видит сейчас: стандартная страница 404

Сначала честно посмотрим на поведение по умолчанию. Запросим у приложения адрес, которого нет, — например /нет-такой или /no-such-page. Настоящий сетевой сервер для этого не нужен: test_client выполняет запрос прямо в Python, без сокетов. Блок запускаемый — вы увидите ровно то, что получил бы браузер:

Запрос несуществующего адреса (запустите)
from flask import Flask

app = Flask(__name__)

client = app.test_client()

r = client.get("/no-such-page")
print(r.status_code)
print(r.data.decode())
Вывод
404
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>

Разберём ответ. Статус 404 — правильный: «ресурс не найден», и с ним Flask справляется сам. Проблема в теле: страница на английском, сверстана системным стилем, не знает ни вашего меню, ни вашего бренда. Гость в недоумении закрывает вкладку. Именно эту страницу мы и заменим своей.

@app.errorhandler(404): своя страница ошибки

Декоратор @app.errorhandler(404) регистрирует функцию, которую Flask вызовет каждый раз, когда собрался отдать 404. Функция получает описание ошибки аргументом и возвращает обычный ответ — как view-функция. Запустите:

Свой обработчик 404 (запустите)
from flask import Flask

app = Flask(__name__)

@app.errorhandler(404)
def page_not_found(e):
    return "Страница не найдена. Проверьте адрес или вернитесь на главную.", 404

@app.route("/")
def index():
    return "Главная страница кофейни"

client = app.test_client()

r = client.get("/")
print(r.status_code, r.data.decode())

r = client.get("/coffe/9")
print(r.status_code)
print(r.data.decode())
Вывод
200 Главная страница кофейни
404
Страница не найдена. Проверьте адрес или вернитесь на главную.

Три детали, на которые стоит смотреть внимательно. Первая: аргумент e — это объект ошибки (NotFound), Flask передаёт его сам; обычно он не нужен, но параметр в функции должен быть. Вторая: возвращается кортеж — текст и статус 404. Статус обязателен: без него Flask ответил бы 200, то есть соврал бы браузеру, что всё хорошо. Третья: обработчик сработал, но обычные маршруты не заметили — «Главная страница» по-прежнему открывается со статусом 200.

Возвращать можно всё то же, что и из view-функции:

  • строку и кортеж (текст, 404) — как в примере выше
  • render_template("errors/404.html") — полноценную страницу с меню и стилями, чуть ниже покажем
  • jsonify(error="not found"), 404 — если у вас API и клиент ждёт JSON, пригодится в уроке 14
  • редирект на главную — спорный вариант: гость не поймёт, что искал несуществующее

abort(404): ресурса нет — говорим об этом

404 бывает не только от опечатки в адресе. Маршрут сработал, конвертер распознал номер — но записи с таким номером в базе нет: гость открыл заметку, которую удалили. Это тоже 404, только возникает он уже внутри вашей функции. Для таких случаев у Flask есть abort: он бросает исключение, останавливает view-функцию и попадает в тот же обработчик ошибок:

abort, когда записи нет (запустите)
from flask import Flask, abort

app = Flask(__name__)

notes = {1: "Купить кофе", 2: "Позвонить в сервис"}

@app.route("/notes/<int:note_id>")
def get_note(note_id):
    if note_id not in notes:
        abort(404)  # дальше код не выполняется
    return notes[note_id]

@app.errorhandler(404)
def page_not_found(e):
    return "Такой записи нет", 404

client = app.test_client()

r = client.get("/notes/1")
print(r.status_code, r.data.decode())

r = client.get("/notes/42")
print(r.status_code, r.data.decode())
Вывод
200 Купить кофе
404 Такой записи нет

Обратите внимание, как это читается: «если записи нет — прерви». Альтернатива без abort громоздче: пришлось бы в каждой ветке возвращать ответ с ошибкой и следить, чтобы нормальный код не выполнился после неё. abort принимает любой HTTP-статус: abort(403) — «нет доступа», abort(400) — «плохой запрос», и у каждого может быть свой обработчик.

У abort есть и второй талант — собственный текст: abort(404, description="Записи с таким номером нет"). Описание попадает в объект ошибки, и стандартная страница Flask покажет его вместо общей фразы; ваш обработчик тоже может прочитать его в атрибуте e.description и подставить в шаблон. Для ситуаций вида «файл не найден» и «нет прав» это самый короткий путь к понятному сообщению.

abort с описанием: объяснение прямо в ошибке
@app.route("/files/<path:name>")
def get_file(name):
    if not storage.exists(name):
        abort(404, description="Такого файла в хранилище нет")
    return storage.read(name)

@app.errorhandler(404)
def page_not_found(e):
    return render_template("errors/404.html", hint=e.description), 404
storage здесь — условное хранилище проекта. Смысл блока в связке: description из abort доезжает до обработчика в атрибуте e.description и показывается гостю через переменную hint в шаблоне.

@app.errorhandler(500): когда код упал

Теперь сценарий, за который краснеют: в view-функции случилось необработанное исключение — опечатка, недоступная база, деление на ноль. Flask не пускает traceback к гостю (и хорошо), но подставляет ту же системную заглушку. Заменим и её:

Свой обработчик 500 (запустите)
from flask import Flask

app = Flask(__name__)

@app.route("/brew")
def brew():
    raise RuntimeError("кофемашина не отвечает")

@app.errorhandler(500)
def server_error(e):
    return "Внутренняя ошибка сервера. Мы уже чиним!", 500

client = app.test_client()

r = client.get("/brew")
print(r.status_code)
print(r.data.decode())
Вывод
500
Внутренняя ошибка сервера. Мы уже чиним!

Исключение никуда не делось — оно всплывёт в логах сервера, и вы его увидите и почините. Но гость вместо стены технического текста получает спокойное человеческое сообщение и статус 500, по которому мониторинг поймёт, что сломалось на стороне сервера, а не у него в браузере.

Свой шаблон для страницы ошибки

Строка в ответе — уже хорошо, но настоящая страница ошибки выглядит как страница сайта: с меню, стилями и ссылкой на главную. Значит, нужен шаблон из урока 3. Кладём в папку templates файл errors/404.html:

app.py — обработчик с render_template
from flask import Flask, render_template

app = Flask(__name__)

@app.errorhandler(404)
def page_not_found(e):
    return render_template("errors/404.html"), 404

@app.errorhandler(500)
def server_error(e):
    return render_template("errors/500.html"), 500
Сервер в браузере не запускается — это реальный код приложения с ожидаемым поведением. Механику рендера шаблона покажем на чистом Jinja2 ниже — она запустится прямо здесь.
templates/errors/404.html
{% extends "base.html" %}

{% block content %}
<h1>404 — страница не найдена</h1>
<p>Проверьте адрес или загляните в <a href="{{ url_for('index') }}">каталог кофе</a>.</p>
{% endblock %}
Шаблон наследует base.html — меню и подвал достаются странице бесплатно, гость не выпадает из сайта. url_for строит ссылку на маршрут по имени функции.

А теперь то же самое в движении: отрендерим обе страницы ошибок на чистом Jinja2 — прямо на этой странице. Здесь шаблон записан строкой вместо файла, но выражения {{ }} работают один в один:

Рендер страниц ошибок (запустите)
from jinja2 import Template

error_page = Template("""<!doctype html>
<h1>Ошибка {{ code }}</h1>
<p>{{ message }}</p>
<p><a href="/">На главную</a></p>""")

print(error_page.render(code=404, message="Такой страницы нет. Проверьте адрес."))
print(error_page.render(code=500, message="Что-то сломалось. Мы уже чиним."))
Вывод
<!doctype html>
<h1>Ошибка 404</h1>
<p>Такой страницы нет. Проверьте адрес.</p>
<p><a href="/">На главную</a></p>
<!doctype html>
<h1>Ошибка 500</h1>
<p>Что-то сломалось. Мы уже чиним.</p>
<p><a href="/">На главную</a></p>

Одним шаблоном накрыли обе ошибки: код и сообщение приходят переменными, а обработчики подставляют свои значения. В реальном проекте этот текст лежал бы в templates/errors/404.html и наследовал бы base.html — разницы в механике никакой.

Проверяем себя: статусы через test_client

Как убедиться, что страницы ошибок действительно работают, не открывая браузер и не тыкая адреса руками? test_client: он выполняет запрос к приложению и возвращает объект со статусом и телом. Сводная таблица того, что мы построили:

ЗапросЧто происходитСтатус
client.get("/")маршрут существует, всё хорошо200
client.get("/нет-такой")адрес не совпал ни с одним маршрутом404
client.get("/notes/42")маршрут есть, записи 42 в базе нет — abort(404)404
client.get("/brew")в функции случилось необработанное исключение500

Обработчиков может быть сколько угодно: errorhandler(403) для страниц без доступа, errorhandler(404) из этого урока, errorhandler(500) для падений, даже обработчик для конкретного класса исключений — например, чтобы поймать ошибку базы и показать «база временно недоступна». Flask выбирает обработчик по типу проблемы и не мешает их сочетать. Главное правило одно для всех: возвращайте кортеж с честным статусом.

Итоги: сайт больше не молчит чужими словами

  • Стандартные страницы 404 и 500 — английские заглушки без вашего оформления; их стоит заменить
  • @app.errorhandler(404) регистрирует свою функцию-обработчик для конкретного статуса
  • abort(404) — для ситуации «маршрут есть, ресурса нет»: бросает исключение и попадает в обработчик
  • Обработчик возвращает кортеж (ответ, статус) — статус обязателен, иначе браузеру соврёшь про 200
  • Шаблоны errors/404.html и errors/500.html с наследованием base.html делают страницу ошибки частью сайта
  • test_client проверяет всё без браузера: запрос — и сразу статус ответа

Остался последний штрих: страницы ошибок показывают секреты конфигурации — режим отладки, ключи, адреса баз. Куда их прятать и почему хардкод секрета в коде — беда, разбираем в уроке 12 про конфигурацию Flask.

abort(404) останавливает view-функцию сразу: строки после него не выполняются, а Flask показывает страницу ошибки.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

from flask import Flask, abort

app = Flask(__name__)

@app.route("/n/<int:n>")
def show(n):
    if n > 10:
        abort(404)
    return "ok"

@app.errorhandler(404)
def not_found(e):
    return "нет", 404

client = app.test_client()
r = client.get("/n/50")
print(r.status_code, r.data.decode())
from flask import Flask

app = Flask(__name__)

@app.errorhandler(500)
def e500(e):
    return "поломка", 500

@app.route("/bad")
def bad():
    return 1 / 0

client = app.test_client()
print(client.get("/bad").status_code)
Проверь себя
0 / 6

1. Что видит гость при опечатке в адресе, если приложение без своих обработчиков?

2. Что делает @app.errorhandler(404)?

3. Зачем abort(404) внутри view-функции?

4. Какой статус вернёт маршрут, в котором случилось необработанное исключение?

5. Почему в return обработчика нужен второй элемент кортежа — статус 404?

6. Как быстро проверить, что страница 404 работает, без браузера?

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

Соберите мини-каталог с честными ошибками: маршрут /items/<int:item_id> возвращает товар из словаря items, а если товара нет — вызывает abort(404). Добавьте обработчик @app.errorhandler(404), который возвращает «Товар не найден» со статусом 404. В конце сделайте два запроса через test_client: за товаром 1 и за товаром 50, напечатайте статус и тело каждого.

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

Как сделать свою страницу 404 во Flask?

Зарегистрируйте обработчик: @app.errorhandler(404), внутри верните render_template("errors/404.html"), 404. Шаблон положите в папку templates/errors и унаследуйте от base.html, чтобы страница ошибки была с вашим меню и стилями. Проверить результат можно через test_client: client.get("/нет-такой").status_code должен вернуть 404.

Чем abort(404) отличается от return "...", 404?

abort(404) бросает исключение и немедленно прерывает функцию — удобно в начале, когда ресурса нет, и все проверки идут до основной логики. Возврат кортежа — обычный return: функция продолжит выполнение после него, если не следить за ветками. Для «ресурса не существует» идиоматичнее abort.

Почему вместо моей страницы 500 гость видит traceback?

Значит, включён режим отладки (DEBUG=True) или тестовый режим (TESTING=True): в них Flask поднимает исключение дальше, чтобы разработчик увидел его целиком, и обработчик 500 не вызывается. На продакшене отладку выключают — и гости получают вашу заглушку, а traceback остаётся в логах сервера.

Нужны ли отдельные обработчики для 403 и 400?

Когда эти статусы у вас появляются — да: @app.errorhandler(403) для «нет доступа» и @app.errorhandler(400) для некорректных запросов устроены точно так же, как 404. abort принимает любой HTTP-статус, и у каждого может быть своя страница с человеческим объяснением.

Понравился урок? Сошлитесь на него

«abort(404) останавливает view-функцию сразу: строки после него не выполняются, а Flask показывает страницу ошибки.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

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

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

Проверьте знания по Flask

В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.

Тест по Flask: 20 задач