处理应用程序错误

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

  • 客户端提前终止了请求,应用程序仍在读取传入数据

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

  • 文件系统已满

  • 硬盘坏了

  • 后台服务器过载

  • 正在使用的库出现程序错误

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

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

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

错误日志记录工具

如果有足够多的用户点击了错误,并且日志文件通常从未被查看过,那么发送错误邮件(即使只是针对关键邮件)可能会变得非常困难。这就是为什么我们建议使用 Sentry 用于处理应用程序错误。它可以作为源代码可用的项目使用 on GitHub 也可作为 hosted version 你可以免费试用。Sentry聚合重复的错误,捕获完整的堆栈跟踪和用于调试的本地变量,并根据新的错误或频率阈值向您发送邮件。

要使用岗哨,您需要安装 sentry-sdk 额外客户 flask 依赖关系。

$ pip install sentry-sdk[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,从那里你可以收到错误通知。

参见:

错误处理程序

当烧瓶中发生错误时,适当的 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 状态码,出错处理器可被用于任何异常类的注册。出错处理器可被用于特定类的注册,也可用于一个父类的所有子类的注册。

处理

在构建烧瓶应用程序时 will 遇到异常。如果在处理请求时代码的某些部分中断(并且您没有注册错误处理程序),则会出现“500内部服务器错误” (InternalServerError )将默认返回。同样,“404未找到” (NotFound )如果将请求发送到未注册的路由,将发生错误。如果路由接收到不允许的请求方法,“405方法不允许” (MethodNotAllowed )将被提高。这些都是 HTTPException 默认在烧瓶中提供。

Flask使您能够引发由Werkzeug注册的任何HTTP异常。但是,默认的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中,它将捕获 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 , the Exception 处理程序无法处理 HTTPException 子类,因为它 HTTPException 处理程序更具体。

未处理的异常

当没有为异常注册错误处理程序时,将返回500个内部服务器错误。看到了吗 flask.Flask.handle_exception() 有关此行为的信息。

如果有注册的错误处理程序 InternalServerError ,这将被调用。从Flask1.1.0开始,此错误处理程序将始终传递 InternalServerError ,而不是原始的未处理错误。

原始错误可用作 e.original_exception . 在Werkzeug 1.0.0之前,此属性仅在未处理的错误期间存在,请使用 getattr 以获取兼容性。

@app.errorhandler(InternalServerError)
def handle_500(e):
    original = getattr(e, "original_exception", None)

    if original is None:
        # direct 500 error, such as abort(500)
        return render_template("500.html"), 500

    # wrapped unhandled error
    return render_template("500_unhandled.html", e=original), 500

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

自定义错误页

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

根据错误代码的不同,用户实际看到此类错误的可能性越来越小。

考虑下面的代码,我们可能有一个用户配置文件路由,如果用户未能传递用户名,我们可以发出“400个错误请求”。如果用户传递了一个用户名而我们找不到它,我们将发出一个“404notfound”。

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 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 在blueprint的另一个视图函数中;它们不是由无效的URL访问调用的。

这是因为blueprint不“拥有”某个URL空间,因此应用程序实例无法知道如果给定的URL无效,它应该运行哪个blueprint错误处理程序。如果您想基于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

以JSON形式返回API错误

在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 参数。

日志

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

调试

调试应用程序错误 有关如何调试开发和生产中的错误的信息。