Обработка ошибок приложения
Приложения и сервера могут выходить из строя. Рано или поздно вы столкнетесь с исключением в рабочей среде. Даже если ваш код на 100% корректен, исключения всё же могут возникнуть время от времени. Почему? Потому что всё остальное, что задействовано, может выйти из строя. Вот некоторые ситуации, в которых совершенно исправный код может привести к ошибкам сервера:
- клиент прервал запрос на ранней стадии, и приложение всё ещё читало входящие данные
- сервер базы данных был перегружен и не смог обработать запрос
- полностью заполнен файловый накопитель
- жесткий диск вышел из строя
- перегрузка сервера бэкенда
- ошибка программирования в используемой библиотеке
- отказ сетевого соединения сервера с другой системой
И это лишь небольшой пример проблем, с которыми вы можете столкнуться. Так как же справиться с подобными проблемами? По умолчанию, если ваше приложение работает в режиме производства, и возникает исключение, Flask отобразит для вас очень простую страницу и запишет исключение в журнал в logger.
Но вы можете сделать больше, и мы рассмотрим лучшие настройки для обработки ошибок, включая пользовательские исключения и инструменты сторонних разработчиков.
Инструменты ведения журнала ошибок
Отправка писем об ошибках, даже только для критических, может стать непосильной задачей, если много пользователей сталкиваются с ошибкой, а файлы журнала, как правило, никогда не просматриваются. Вот почему мы рекомендуем использовать Sentry для обработки ошибок приложения. Он доступен как проект с открытым исходным кодом на GitHub, а также как хостируемая версия, которую вы можете попробовать бесплатно. Sentry агрегирует дублирующиеся ошибки, сохраняет полный стек вывода и локальные переменные для отладки и отправляет вам письма на основе новых ошибок или пороговых значений частоты.
Для использования Sentry необходимо установить sentry-sdk клиент с дополнительными flask зависимостями.
$ pip install sentry-sdk[flask]
И затем добавьте это в ваше приложение Flask:
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()])
Значение YOUR_DSN_HERE необходимо заменить на значение DSN, которое вы получите из вашей установки Sentry.
После установки ошибки, приводящие к ошибке «Внутренняя ошибка сервера», автоматически сообщаются в Sentry, и оттуда вы можете получать уведомления об ошибках.
См. также:
- Sentry также поддерживает перехват ошибок из очереди задач (RQ, Celery и т. д.) аналогичным образом. Подробнее см. в документации Python SDK Python SDK docs.
- Начало работы с Sentry
- Документация, специфичная для Flask
Обработчики ошибок
Когда в Flask возникает ошибка, будет возвращен соответствующий код HTTP-статуса. Коды 400-499 указывают на ошибки в данных запроса клиента или в запрошенных данных. Коды 500-599 указывают на ошибки самого сервера или приложения.
Вы можете отображать пользовательские страницы ошибок, когда возникает ошибка. Это можно сделать, зарегистрировав обработчики ошибок.
Обработчик ошибок — это функция, которая возвращает ответ при возникновении определенного типа ошибки, аналогично тому, как представление — это функция, которая возвращает ответ при соответствии запросу URL. Ему передается экземпляр обрабатываемой ошибки, которая, скорее всего, является HTTPException.
Код статуса ответа не будет установлен в код обработчика. Убедитесь, что при возвращении ответа из обработчика вы предоставляете соответствующий код HTTP-статуса.
Регистрация
Регистрируйте обработчики, используя декоратор errorhandler(). Или используйте register_error_handler() для регистрации функции позже. Не забудьте установить код ошибки при возвращении ответа.
@app.errorhandler(werkzeug.exceptions.BadRequest)
def handle_bad_request(e):
return 'bad request!', 400
# or, without the decorator
app.register_error_handler(400, handle_bad_request)
werkzeug.exceptions.HTTPException подклассы, такие как BadRequest, и их коды HTTP взаимозаменяемы при регистрации обработчиков. (BadRequest.code == 400)
Нестандартные коды HTTP не могут быть зарегистрированы кодом, потому что они не известны Werkzeug. Вместо этого определите подкласс HTTPException с соответствующим кодом и зарегистрируйте, и вызовите этот класс исключения.
class InsufficientStorage(werkzeug.exceptions.HTTPException):
code = 507
description = 'Not enough storage space.'
app.register_error_handler(InsufficientStorage, handle_507)
raise InsufficientStorage()
Обработчики могут быть зарегистрированы для любого класса исключений, а не только для подклассов HTTPException или кодов HTTP-статусов. Обработчики могут быть зарегистрированы для определённого класса или для всех подклассов родительского класса.
Обработка
При создании приложения Flask вы будете сталкиваться с исключениями. Если какая-то часть вашего кода выйдет из строя во время обработки запроса (и у вас не зарегистрированы обработчики ошибок), по умолчанию будет возвращён «500 Внутренняя ошибка сервера» (InternalServerError). Аналогично, ошибка «404 Не найдено» (NotFound) возникнет, если запрос отправлен на незарегистрированный маршрут. Если маршрут получает неразрешённый метод запроса, будет вызвано «405 Метод запрещён» (MethodNotAllowed). Все они являются подклассами HTTPException и предоставляются по умолчанию в Flask.
Flask предоставляет возможность вызывать любое исключение HTTP, зарегистрированное Werkzeug. Однако, стандартные исключения HTTP возвращают простые страницы исключений. Вы можете отображать пользовательские страницы ошибок, когда возникает ошибка. Это можно сделать, зарегистрировав обработчики ошибок.
Когда Flask перехватывает исключение во время обработки запроса, сначала выполняется поиск по коду. Если для кода не зарегистрирован обработчик, Flask ищет ошибку по иерархии классов; выбирается наиболее конкретный обработчик. Если обработчик не зарегистрирован, подклассы HTTPException отображают общее сообщение об их коде, а другие исключения преобразуются в общую ошибку «500 Внутренняя ошибка сервера».
Например, если возникает экземпляр ConnectionRefusedError, и зарегистрирован обработчик для ConnectionError и ConnectionRefusedError, вызывается более конкретный обработчик ConnectionRefusedError с экземпляром исключения для генерации ответа.
Обработчики, зарегистрированные в модуле, имеют приоритет над теми, которые зарегистрированы глобально в приложении, при условии, что модуль обрабатывает запрос, который вызывает исключение. Однако модуль не может обрабатывать ошибки маршрутизации 404, поскольку ошибка 404 происходит на уровне маршрутизации до определения модуля.
Обработчики общих исключений
Можно зарегистрировать обработчики ошибок для очень общих базовых классов, таких как HTTPException или даже Exception. Однако следует помнить, что это перехватит больше, чем вы ожидаете.
Например, обработчик ошибок для HTTPException может быть полезен для преобразования стандартных страниц HTML об ошибках в JSON. Однако этот обработчик сработает для ситуаций, которые вы не вызываете напрямую, таких как ошибки 404 и 405 при маршрутизации. Убедитесь, что ваш обработчик тщательно разработан, чтобы не потерять информацию об ошибке HTTP.
from flask import json
from werkzeug.exceptions import HTTPException
@app.errorhandler(HTTPException)
def handle_exception(e):
"""Return JSON instead of HTML for HTTP errors."""
# start with the correct headers and status code from the error
response = e.get_response()
# replace the body with JSON
response.data = json.dumps({
"code": e.code,
"name": e.name,
"description": e.description,
})
response.content_type = "application/json"
return response
Обработчик ошибок для Exception может показаться полезным для изменения способа представления всех ошибок, даже необработанных, пользователю. Однако это похоже на выполнение except Exception: в Python, он перехватит все в противном случае необработанные ошибки, включая все коды HTTP-статуса.
В большинстве случаев безопаснее регистрировать обработчики для более конкретных исключений. Поскольку экземпляры HTTPException являются допустимыми ответами WSGI, вы также можете передавать их непосредственно.
from werkzeug.exceptions import HTTPException
@app.errorhandler(Exception)
def handle_exception(e):
# pass through HTTP errors
if isinstance(e, HTTPException):
return e
# now you're handling non-HTTP exceptions only
return render_template("500_generic.html", e=e), 500
Обработчики ошибок всё ещё учитывают иерархию классов исключений. Если вы зарегистрируете обработчики как для HTTPException так и для Exception, обработчик Exception не будет обрабатывать подклассы HTTPException, так как обработчик HTTPException более конкретный.
Необработанные исключения
Если для исключения не зарегистрирован обработчик ошибок, возвращается ошибка 500 Внутренняя ошибка сервера. См. flask.Flask.handle_exception() для информации об этом поведении.
Если зарегистрирован обработчик ошибок для InternalServerError, он будет вызван. Начиная с Flask 1.1.0, этот обработчик ошибок всегда получает экземпляр InternalServerError, а не исходную необработанную ошибку.
Исходная ошибка доступна как e.original_exception.
Обработчик ошибок для «500 Внутренняя ошибка сервера» получит необработанные исключения в дополнение к явным ошибкам 500. В режиме отладки обработчик для «500 Внутренняя ошибка сервера» не будет использоваться. Вместо этого будет показан интерактивный отладчик.
Пользовательские страницы ошибок
Иногда при создании приложения Flask вам может понадобиться вызвать HTTPException, чтобы сообщить пользователю, что с запросом что-то не так. К счастью, Flask предоставляет удобную функцию abort(), которая прерывает запрос с HTTP-ошибкой из Werkzeug, как нужно. Она также предоставит вам простую черно-белую страницу с ошибкой с базовым описанием, но без изысков.
В зависимости от кода ошибки, пользователь может или не может увидеть такую ошибку.
Рассмотрим пример кода ниже; у нас может быть маршрут профиля пользователя, и если пользователь не передаст имя пользователя, мы можем выбросить «400 Bad Request». Если пользователь передаст имя пользователя, и мы не сможем его найти, мы выбросим «404 Not Found».
from flask import abort, render_template, request
# a username needs to be supplied in the query args
# a successful request would be like /profile?username=jack
@app.route("/profile")
def user_profile():
username = request.arg.get("username")
# if a username isn't supplied in the request, return a 400 bad request
if username is None:
abort(400)
user = get_user(username=username)
# if a user can't be found by their username, return 404 not found
if user is None:
abort(404)
return render_template("profile.html", user=user)
Вот ещё один пример реализации исключения «404 Page Not Found»:
from flask import render_template
@app.errorhandler(404)
def page_not_found(e):
# note that we set the 404 status explicitly
return render_template('404.html'), 404
При использовании Фабрик приложений:
from flask import Flask, render_template
def page_not_found(e):
return render_template('404.html'), 404
def create_app(config_filename):
app = Flask(__name__)
app.register_error_handler(404, page_not_found)
return app
Пример шаблона может быть таким:
{% extends "layout.html" %}
{% block title %}Page Not Found{% endblock %}
{% block body %}
<h1>Page Not Found</h1>
<p>What you were looking for is just not there.
<p><a href="{{ url_for('index') }}">go somewhere nice</a>
{% endblock %}
Дополнительные примеры
Вышеприведённые примеры не будут улучшением по умолчанию страниц с ошибками. Мы можем создать пользовательский шаблон 500.html так:
{% extends "layout.html" %}
{% block title %}Internal Server Error{% endblock %}
{% block body %}
<h1>Internal Server Error</h1>
<p>Oops... we seem to have made a mistake, sorry!</p>
<p><a href="{{ url_for('index') }}">Go somewhere nice instead</a>
{% endblock %}
Его можно реализовать, рендерив шаблон при ошибке «500 Internal Server Error»:
from flask import render_template
@app.errorhandler(500)
def internal_server_error(e):
# note that we set the 500 status explicitly
return render_template('500.html'), 500
При использовании Фабрик приложений:
from flask import Flask, render_template
def internal_server_error(e):
return render_template('500.html'), 500
def create_app():
app = Flask(__name__)
app.register_error_handler(500, internal_server_error)
return app
При использовании Модульных приложений с Blueprint:
from flask import Blueprint
blog = Blueprint('blog', __name__)
# as a decorator
@blog.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
# or with register_error_handler
blog.register_error_handler(500, internal_server_error)
Обработчики ошибок Blueprint
В Модульных приложениях с Blueprint большинство обработчиков ошибок будут работать как ожидается. Однако существует оговорка, касающаяся обработчиков исключений 404 и 405. Эти обработчики вызываются только из соответствующего raise или вызова abort в другой функции представления blueprint; они не вызываются, например, при доступе к недопустимому URL.
Это потому, что blueprint не «владеет» определённым пространством URL, поэтому экземпляр приложения не знает, какой обработчик ошибок blueprint следует запустить при получении недопустимого URL. Если вам нужно выполнить различные стратегии обработки для этих ошибок на основе префиксов URL, их можно определить на уровне приложения с помощью request прокси-объекта.
from flask import jsonify, render_template
# at the application level
# not the blueprint level
@app.errorhandler(404)
def page_not_found(e):
# if a request is in our blog URL space
if request.path.startswith('/blog/'):
# we return a custom blog 404 page
return render_template("blog/404.html"), 404
else:
# otherwise we return our generic site-wide 404 page
return render_template("404.html"), 404
@app.errorhandler(405)
def method_not_allowed(e):
# if a request has the wrong method to our API
if request.path.startswith('/api/'):
# we return a json saying so
return jsonify(message="Method Not Allowed"), 405
else:
# otherwise we return a generic site-wide 405 page
return render_template("405.html"), 405
Возвращение ошибок API в формате JSON
При создании API в Flask некоторые разработчики понимают, что встроенных исключений недостаточно для API, и что тип контента text/html, который они генерируют, не очень полезен для потребителей API.
Используя те же приёмы, что и выше, и jsonify(), мы можем возвращать JSON-ответы на ошибки API. abort() вызывается с параметром description. Обработчик ошибок будет использовать его в качестве JSON-сообщения об ошибке и установит код состояния в 404.
from flask import abort, jsonify
@app.errorhandler(404)
def resource_not_found(e):
return jsonify(error=str(e)), 404
@app.route("/cheese")
def get_one_cheese():
resource = get_resource()
if resource is None:
abort(404, description="Resource not found")
return jsonify(resource)
Мы также можем создать пользовательские классы исключений. Например, мы можем ввести новый пользовательский класс исключения для API, который может принимать правильное читаемое человеком сообщение, код состояния для ошибки и некоторые необязательные данные, чтобы предоставить дополнительный контекст ошибки.
Вот простой пример:
from flask import jsonify, request
class InvalidAPIUsage(Exception):
status_code = 400
def __init__(self, message, status_code=None, payload=None):
super().__init__()
self.message = message
if status_code is not None:
self.status_code = status_code
self.payload = payload
def to_dict(self):
rv = dict(self.payload or ())
rv['message'] = self.message
return rv
@app.errorhandler(InvalidAPIUsage)
def invalid_api_usage(e):
return jsonify(e.to_dict())
# an API app route for getting user information
# a correct request might be /api/user?user_id=420
@app.route("/api/user")
def user_api(user_id):
user_id = request.arg.get("user_id")
if not user_id:
raise InvalidAPIUsage("No user id provided!")
user = get_user(user_id=user_id)
if not user:
raise InvalidAPIUsage("No such user!", status_code=404)
return jsonify(user.to_dict())
Теперь представление может вызвать это исключение с сообщением об ошибке. Кроме того, некоторые дополнительные данные могут быть переданы как словарь через параметр payload.
Ведение журнала
См. Ведение журнала для получения информации о том, как регистрировать исключения, например, отправляя их по электронной почте администраторам.
Отладка
См. Отладка ошибок приложения для получения информации о том, как отлаживать ошибки в режиме разработки и производства.
© 2007–2021 Pallets
Licensed under the BSD 3-clause License.
https://flask.palletsprojects.com/en/2.0.x/errorhandling/