处理应用程序错误

应用程序会失败,服务器会失败。迟早你会在生产中看到异常。即使你的代码 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 值替换为您从 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 未找到”(NotFound)错误。如果路由接收到不允许的请求方法,则会引发“405 方法不允许”(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:,它将捕获所有其他未处理的错误,包括所有 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 内部服务器错误。有关此行为的信息,请参见 flask.Flask.handle_exception()

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

原始错误可作为 e.original_exception 获得。

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

自定义错误页面

有时在构建 Flask 应用程序时,您可能希望引发 HTTPException,以向用户发出信号,表明请求出现问题。幸运的是,Flask 附带了一个方便的 abort() 函数,该函数会根据需要中止带有 werkzeug HTTP 错误的请求。它还将为您提供一个带有基本描述的纯黑白错误页面,但没有任何花哨的东西。

根据错误代码,用户实际上看到此类错误的可能性或多或少。

考虑以下代码,我们可能有一个用户个人资料路由,如果用户未能传递用户名,我们可以引发“400 错误请求”。如果用户传递了用户名,但我们找不到它,我们会引发“404 未找到”。

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 页面未找到”异常的另一个示例实现

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 内部服务器错误”上呈现模板来实现它

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 错误。使用 description 参数调用 abort()。错误处理程序会将该参数用作 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 参数将一些额外的有效负载作为字典提供。

日志记录

请参阅 日志记录,了解有关如何记录异常(例如通过电子邮件将它们发送给管理员)的信息。

调试

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