Spec-Zone.ru › Flask 2.1

Обработка ошибок приложения

Приложения выходят из строя, серверы выходят из строя. Рано или поздно в рабочей среде вы увидите исключение. Даже если ваш код на 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.
  • Начало работы с 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

При использовании Модульных приложений с Blueprints:

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

В Модульных приложениях с Blueprints большинство обработчиков ошибок будут работать как ожидается. Однако есть оговорка, касающаяся обработчиков исключений 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()), e.status_code

# 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–2022 Pallets
Licensed under the BSD 3-clause License.
https://flask.palletsprojects.com/en/2.1.x/errorhandling/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API