Обработка ошибок во 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-функция. Запустите:
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-функцию и попадает в тот же обработчик ошибок:
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 и подставить в шаблон. Для ситуаций вида «файл не найден» и «нет прав» это самый короткий путь к понятному сообщению.
@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
@app.errorhandler(500): когда код упал
Теперь сценарий, за который краснеют: в view-функции случилось необработанное исключение — опечатка, недоступная база, деление на ноль. Flask не пускает traceback к гостю (и хорошо), но подставляет ту же системную заглушку. Заменим и её:
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:
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
{% extends "base.html" %}
{% block content %}
<h1>404 — страница не найдена</h1>
<p>Проверьте адрес или загляните в <a href="{{ url_for('index') }}">каталог кофе</a>.</p>
{% endblock %}
А теперь то же самое в движении: отрендерим обе страницы ошибок на чистом 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)
1. Что видит гость при опечатке в адресе, если приложение без своих обработчиков?
2. Что делает @app.errorhandler(404)?
3. Зачем abort(404) внутри view-функции?
4. Какой статус вернёт маршрут, в котором случилось необработанное исключение?
5. Почему в return обработчика нужен второй элемент кортежа — статус 404?
6. Как быстро проверить, что страница 404 работает, без браузера?
Соберите мини-каталог с честными ошибками: маршрут /items/<int:item_id> возвращает товар из словаря items, а если товара нет — вызывает abort(404). Добавьте обработчик @app.errorhandler(404), который возвращает «Товар не найден» со статусом 404. В конце сделайте два запроса через test_client: за товаром 1 и за товаром 50, напечатайте статус и тело каждого.
Как сделать свою страницу 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Flask · Урок 2
Первое веб-приложение на Flask: маршруты и режим отладки
Второй урок курса Flask: несколько страниц в одном приложении, динамические маршруты /post/42, конвертеры типов, своя страница 404 и debug=True, который экономит перезапуски.
Flask · Урок 3
Шаблоны Jinja2: HTML-страницы с данными из Python
Третий урок курса Flask: шаблоны Jinja2 — переменные, циклы, фильтры и наследование base.html. Отличие этого урока: все примеры Jinja2 можно отрендерить прямо на странице.
Flask · Урок 12
Конфигурация Flask: config-классы и переменные окружения
Двенадцатый урок курса Flask: настройки живут не в коде, а в конфигурации. Собираем класс Config, читаем SECRET_KEY из переменных окружения и переключаем приложение в тестовый режим одной строкой app.config.update(TESTING=True).
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
aiogram · Урок 9
Обработка ошибок в телеграм-боте: try/except, лимиты и логи
Учим бот переживать ошибки: try/except в хендлерах, лимиты Telegram с RetryAfter, модуль logging вместо print и перезапуск polling после падения.
обработка ошибок pythonошибки телеграм бота
pytest · Урок 9
Проверка исключений: pytest.raises
Хорошая функция на плохом входе не молчит, а бросает исключение — и тест обязан это проверять. pytest.raises делает это одной строкой внутри with.
pytest.raises проверка исключенийтестирование исключений python
Flask · Урок 14
Валидация в Flask API: плохие запросы и коды 400
Четырнадцатый урок курса Flask: API из урока 13 падает на первом же кривом запросе. Добавляем валидацию — честные 400 с понятным текстом ошибки, 404 для отсутствующих записей и тесты на плохие входы через test_client.
flask api валидация 400flask 400 bad request
Проверьте знания по Flask
В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по Flask: 20 задач