处理应用程序错误

应用程序会失败,服务器也会失败。迟早您会在生产环境中看到异常。即使您的代码 100% 正确,您仍然会不时看到异常。为什么?因为其他所有相关的事物都可能失败。以下是一些即使代码完全正常也可能导致服务器错误的情况

  • 客户端过早终止了请求,而应用程序仍在从传入数据中读取

  • 数据库服务器过载,无法处理查询

  • 文件系统已满

  • 硬盘驱动器崩溃

  • 后端服务器过载

  • 您正在使用的库中存在编程错误

  • 服务器到另一个系统的网络连接失败

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

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

错误日志记录工具

发送错误邮件,即使只是针对关键错误,如果足够多的用户遇到错误,并且日志文件通常无人查看,也可能会变得不堪重负。这就是为什么我们建议使用 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 未找到”(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 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 内部服务器错误”上渲染模板来实现

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()), 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 参数以字典形式提供一些额外的有效负载。

日志记录

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

调试

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