处理应用程序错误 — Flask 文档

来自菜鸟教程
Flask/docs/2.0.x/errorhandling
跳转至:导航、​搜索

处理应用程序错误

应用程序失败,服务器失败。 迟早你会在生产中看到异常。 即使您的代码是 100% c 正确,您仍然会不时看到异常。 为什么? 因为其他涉及的一切都会失败。 以下是一些完美的代码可能导致服务器错误的情况:

  • 客户端提前终止请求,应用程序仍在读取传入的数据
  • 数据库服务器过载,无法处理查询
  • 文件系统已满
  • 硬盘坏了
  • 后端服务器过载
  • 您正在使用的库中的编程错误
  • 服务器到另一个系统的网络连接失败

这只是您可能面临的一小部分问题。 那么我们如何处理这样的问题呢? 默认情况下,如果您的应用程序在生产模式下运行,并引发异常 Flask 将为您显示一个非常简单的页面并将异常记录到 logger

但是您可以做的还有更多,我们将介绍一些更好的设置来处理错误,包括自定义异常和 3rd 方工具。

错误记录工具

发送错误邮件,即使只是针对关键邮件,如果有足够多的用户遇到错误并且通常从不查看日志文件,也会变得不堪重负。 这就是我们推荐使用 Sentry 来处理应用程序错误的原因。 它可作为源代码项目 在 GitHub 上使用,也可作为 托管版本 使用,您可以免费试用。 Sentry 聚合重复错误,捕获完整的堆栈跟踪和局部变量以进行调试,并根据新错误或频率阈值向您发送邮件。

要使用 Sentry,您需要安装带有额外 flask 依赖项的 sentry-sdk 客户端。

$ 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 值需要替换为您从 Sentry 安装中获得的 DSN 值。

安装后,导致内部服务器错误的故障会自动报告给 Sentry,您可以从那里收到错误通知。

另请参阅


错误处理程序

当 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 Not Found”(NotFound)错误。 如果路由接收到不允许的请求方法,则会引发“405 Method Not Allowed”(MethodNotAllowed)。 这些都是 HTTPException 的子类,在 Flask 中默认提供。

Flask 使您能够引发 Werkzeug 注册的任何 HTTP 异常。 但是,默认的 HTTP 异常返回简单的异常页面。 您可能希望在发生错误时向用户显示自定义错误页面。 这可以通过注册错误处理程序来完成。

当 Flask 在处理请求时捕捉到异常时,首先通过代码查找。 如果没有为代码注册处理程序,Flask 通过其类层次结构查找错误; 选择最具体的处理程序。 如果未注册处理程序,HTTPException 子类会显示有关其代码的通用消息,而其他异常将转换为通用“500 内部服务器错误”。

例如,如果引发了 ConnectionRefusedError 的实例,并且为 ConnectionErrorConnectionRefusedError 注册了一个处理程序,则使用以下命令调用更具体的 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 的错误处理程序对于更改所有错误(甚至是未处理的错误)呈现给用户的方式似乎很有用。 但是,这类似于在 Python 中执行 except Exception:,它将捕获 all 否则未处理的错误,包括所有 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

错误处理程序仍然尊重异常类层次结构。 如果同时为 HTTPExceptionException 注册处理程序,Exception 处理程序将不会处理 HTTPException 子类,因为 HTTPException 处理程序更多具体的。


未处理的异常

当没有为异常注册错误处理程序时,将返回 500 Internal Server Error。 有关此行为的信息,请参阅 flask.Flask.handle_exception()

如果为 InternalServerError 注册了错误处理程序,则将调用该处理程序。 从 Flask 1.1.0 开始,此错误处理程序将始终传递一个 InternalServerError 实例,而不是原始未处理的错误。

原始错误可用作 e.original_exception

除了明确的 500 错误之外,“500 内部服务器错误”的错误处理程序还将传递未捕获的异常。 在调试模式下,不会使用“500 Internal Server Error”的处理程序。 相反,将显示交互式调试器。


自定义错误页面

有时在构建 Flask 应用程序时,您可能想要引发 HTTPException 以向用户发出信号,表明请求有问题。 幸运的是,Flask 带有一个方便的 abort() 函数,可以根据需要中止来自 werkzeug 的带有 HTTP 错误的请求。 它还将为您提供一个简单的黑白错误页面,其中包含基本描述,但没什么特别的。

根据错误代码,用户实际看到此类错误的可能性较小或较大。

考虑下面的代码,我们可能有一个用户配置文件路由,如果用户未能通过用户名,我们可以提出“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 返回

在 Flask 中构建 API 时,一些开发人员意识到内置异常对于 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 参数作为字典提供。


日志记录

有关如何记录异常的信息,请参阅 Logging,例如通过电子邮件将异常发送给管理员。


调试

有关如何在开发和生产中调试错误的信息,请参阅 调试应用程序错误