Работа с ошибками приложения

Приложения отказывают, серверы отказывают. Рано или поздно вы увидите исключение в производстве. Даже если ваш код на 100% правильный, вы все равно будете время от времени видеть исключения. Почему? Потому что все остальное не работает. Вот несколько ситуаций, когда совершенно правильный код может привести к ошибкам сервера:

  • клиент завершил запрос раньше времени, а приложение все еще считывало входящие данные

  • сервер базы данных был перегружен и не смог обработать запрос

  • файловая система заполнена

  • разбился жесткий диск

  • перегрузка внутреннего сервера

  • программная ошибка в используемой вами библиотеке

  • сетевое подключение сервера к другой системе не удалось

И это лишь малая часть проблем, с которыми вы можете столкнуться. Как же нам справиться с подобными проблемами? По умолчанию, если ваше приложение работает в производственном режиме и возникает исключение, Flask отобразит для вас очень простую страницу и запишет исключение в журнал logger.

Но есть и другие возможности, и мы расскажем о некоторых лучших настройках для работы с ошибками, включая пользовательские исключения и инструменты сторонних производителей.

Инструменты регистрации ошибок

Отправка писем об ошибках, даже если речь идет только о критических ошибках, может стать непосильной, если на ошибку попадает достаточно много пользователей, а файлы журнала, как правило, никогда не просматриваются. Вот почему мы рекомендуем использовать Sentry для работы с ошибками приложений. Он доступен как проект с исходным кодом on GitHub, а также доступен в виде hosted version, который вы можете попробовать бесплатно. 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 docs.

  • Flask-specific documentation

Обработчики ошибок

Когда во Flask возникает ошибка, возвращается соответствующее сообщение HTTP status code. 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 Internal Server Error» (InternalServerError). Аналогично, ошибка «404 Not Found» (NotFound) возникнет, если запрос будет отправлен на незарегистрированный маршрут. Если маршрут получает неразрешенный метод запроса, будет выдана ошибка «405 Method Not Allowed» (MethodNotAllowed). Все эти классы являются подклассами HTTPException и предоставляются по умолчанию во Flask.

Flask дает вам возможность поднять любое исключение HTTP, зарегистрированное Werkzeug. Однако HTTP-исключения по умолчанию возвращают простые страницы исключений. Возможно, вы захотите показывать пользователю пользовательские страницы ошибок при возникновении ошибки. Это можно сделать, зарегистрировав обработчики ошибок.

Когда Flask ловит исключение при обработке запроса, его сначала ищут по коду. Если для кода не зарегистрирован обработчик, Flask ищет ошибку по иерархии классов; выбирается наиболее специфичный обработчик. Если обработчик не зарегистрирован, подклассы HTTPException показывают общее сообщение о своем коде, а другие исключения преобразуются в общее сообщение «500 Internal Server Error».

Например, если возникает экземпляр 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 Internal Server Error» будут передаваться не пойманные исключения в дополнение к явным 500 ошибкам. В режиме отладки обработчик для «500 Internal Server Error» не будет использоваться. Вместо этого будет показан интерактивный отладчик.

Пользовательские страницы ошибок

Иногда при создании 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

При использовании Модульные приложения с чертежами:

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)

Обработчики ошибок чертежей

В Модульные приложения с чертежами большинство обработчиков ошибок будут работать так, как ожидается. Однако существует оговорка относительно обработчиков исключений 404 и 405. Эти обработчики ошибок вызываются только из соответствующего оператора raise или вызова abort в другой функции представления чертежа; они не вызываются, например, при неправильном доступе к URL.

Это происходит потому, что блюпринт не «владеет» определенным пространством URL, поэтому у экземпляра приложения нет возможности узнать, какой обработчик ошибок блюпринта он должен запустить, если ему будет предоставлен недопустимый 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.

Ведение журнала

См. Ведение журнала для получения информации о том, как регистрировать исключения, например, отправляя их по электронной почте администраторам.

Отладка

Смотрите Отладка ошибок приложения для получения информации о том, как отлаживать ошибки в разработке и производстве.

Вернуться на верх