应用错误处理

Changelog

New in version 0.3.

应用出错,服务器出错。或早或晚,你会遇到产品出错。即使你的代码是百分百正确,还是会时常看见出错。为什么?因为其他相关东西会出错。以下是一些在代码完全正确的条件下服务器出错的情况:

  • 客户端已经中断了请求,但应用还在读取数据。

  • 数据库已经过载,无法处理查询。

  • 文件系统没有空间。

  • 硬盘完蛋了。

  • 后台服务过载。

  • 使用的库出现程序错误。

  • 服务器与另一个系统的网络连接出错。

以上只是你会遇到的问题的一小部分。那么如何处理这些问题呢?如果你的应用运行在生产环境下,那么缺省情况下 Flask 会显示一个简单的出错页面,并把出错情况记录到 logger

但可做的还不只这些,下面介绍一些更好的出错处理方法。

错误日志工具

当足够多的用户触发了错误时,发送关于出错信息的邮件,即使仅包含严重错误的邮件也会是一场空难。更不用提从来不会去看的日志文件了。因此,推荐使用 Sentry 来处理应用错误。它可以在一个开源项目 on GitHub 中获得,也可以在 hosted version 中免费试用。Sentry 统计重复错误,捕获堆栈数据和本地变量用于排错,并在发生新的或者指定频度的错误时发送电子邮件。

要使用 Sentry 需要安装带有 flask 依赖的 sentry-sdk 客户端:

  1. $ pip install sentry-sdk[flask]

把下面内容加入 Flask 应用:

  1. import sentry_sdk
  2. from sentry_sdk.integrations.flask import FlaskIntegration
  3. sentry_sdk.init('YOUR_DSN_HERE',integrations=[FlaskIntegration()])

YOUR_DSN_HERE 需要被替换为在 Sentry 安装时获得的 DSN 值。

安装好以后,内部服务出错信息会自动向 Sentry 报告,你会接收到出错通知。

后续阅读:

错误处理

当错误发生时,你可能想要向用户显示自定义的出错页面。注册出错处理器或以做到这点。

一个出错处理器是一个返回响应的普通视图函数。但是不同之在于它不是用于路由的,而是用于一个异常或者当尝试处理请求时抛出 HTTP 状态码。

注册

通过使用 errorhandler() 装饰函数来注册或者稍后使用register_error_handler() 来注册。记得当返回响应的时候设置出错代码:

  1. @app.errorhandler(werkzeug.exceptions.BadRequest)
  2. def handle_bad_request(e):
  3. return 'bad request!', 400
  4. # or, without the decorator
  5. app.register_error_handler(400, handle_bad_request)

当注册时, werkzeug.exceptions.HTTPException 的子类,如BadRequest ,和它们的 HTTP 代码是可替换的。( BadRequest.code == 400

因为 Werkzeug 无法识别非标准 HTTP 代码,因此它们不能被注册。替代地,使用适当的代码定义一个 HTTPException 子类,注册并抛出异常类:

  1. class InsufficientStorage(werkzeug.exceptions.HTTPException):
  2. code = 507
  3. description = 'Not enough storage space.'
  4. app.register_error_handler(InsufficientStorage, handle_507)
  5. raise InsufficientStorage()

出错处理器可被用于任何异常类的注册,除了HTTPException 子类或者 HTTP 状态码。出错处理器可被用于特定类的注册,也可用于一个父类的所有子类的注册。

处理

在处理请求时,当 Flask 捕捉到一个异常时,它首先根据代码检索。如果该代码没有注册处理器,它会根据类的继承来查找,确定最合适的注册处理器。如果找不到已注册的处理器,那么 HTTPException 子类会显示一个关于代码的通用消息。没有代码的异常会被转化为一个通用的 500 内部服务器错误。

例如,如果一个 ConnectionRefusedError 的实例被抛出,并且一个出错处理器注册到 ConnectionErrorConnectionRefusedError ,那么会使用更合适的 ConnectionRefusedError 来处理异常实例,生成响应。

当一个蓝图在处理抛出异常的请求时,在蓝图中注册的出错处理器优先于在应用中全局注册的出错处理器。但是,蓝图无法处理 404 路由错误,因为 404 发生的路由级别还不能检测到蓝图。

通用异常处理器

可以为非常通用的基类注册异常处理器,例如 HTTPException 基类或者甚至Exception 基类。但是,请注意,这样会捕捉到超出你预期的异常。

基于 HTTPException 的异常处理器对于把缺省的 HTML 出错页面转换为 JSON非常有用,但是这个处理器会触发不由你直接产生的东西,如路由过程中产生的404 和 405 错误。请仔细制作你的处理器,确保不会丢失关于 HTTP 错误的信息。

  1. from flask import json
  2. from werkzeug.exceptions import HTTPException
  3. @app.errorhandler(HTTPException)
  4. def handle_exception(e):
  5. """Return JSON instead of HTML for HTTP errors."""
  6. # start with the correct headers and status code from the error
  7. response = e.get_response()
  8. # replace the body with JSON
  9. response.data = json.dumps({
  10. "code": e.code,
  11. "name": e.name,
  12. "description": e.description,
  13. })
  14. response.content_type = "application/json"
  15. return response

基于 Exception 的异常处理器有助于改变所有异常处理的表现形式,甚至包含未处理的异常。但是,与在 Python 使用 except Exception: 类似,这样会捕获 所有 未处理的异常,包括所有 HTTP 状态码。因此,在大多数情况下,设定只针对特定异常的处理器比较安全。因为 HTTPException 实例是一个合法的 WSGI 响应,你可以直接传递该实例。

  1. from werkzeug.exceptions import HTTPException
  2. @app.errorhandler(Exception)
  3. def handle_exception(e):
  4. # pass through HTTP errors
  5. if isinstance(e, HTTPException):
  6. return e
  7. # now you're handling non-HTTP exceptions only
  8. 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_error 访问。在 Werkzeug 1.0.0 以前,这个属性只有未处理异常有。建议使用 getattr 访问这个属性,以保证兼容性。

  1. @app.errorhandler(InternalServerError)
  2. def handle_500(e):
  3. original = getattr(e, "original_exception", None)
  4. if original is None:
  5. # direct 500 error, such as abort(500)
  6. return render_template("500.html"), 500
  7. # wrapped unhandled error
  8. return render_template("500_unhandled.html", e=original), 500

日志

如何记录异常,比如向管理者发送邮件,参见 日志

排除应用错误

应用错误处理 一文所讲的是如何为生产应用设置日志和出错通知。本文要讲的是部署中配置调试的要点和如何使用全功能的 Python 调试器深挖错误。

有疑问时,请手动运行

在生产环境中,配置应用时出错?如果你可以通过 shell 来访问主机,那么首先请在部署环境中验证是否可以通过 shell 手动运行你的应用。请确保验证时使用的帐户与配置的相同,这样可以排除用户权限引发的错误。可以在你的生产服务器上,使用 Flask 内建的开发服务器,并且设置 debug=True ,这样有助于找到配置问题。但是,请 只能在可控的情况下临时这样做 ,绝不能在生产时使用debug=True

使用调试器

为了更深入的挖掘错误,追踪代码的执行, Flask 提供一个开箱即用的调试器(参见 调试模式 )。如果你需要使用其他 Python 调试器,请注意调试器之间的干扰问题。在使用你自己的调试器前要做一些参数调整:

  • debug - 是否开启调试模式并捕捉异常

  • use_debugger - 是否使用 Flask 内建的调试器

  • use_reloader - 模块变化后是否重载并派生进程

debug 必须设置为 True (即必须捕获异常),另两个随便。

如果你正在使用 Aptana 或 Eclipse 排错,那么 use_debuggeruse_reloader 都必须设置为 False 。

一个有用的配置模式如下(当然要根据你的应用调整缩进):

  1. FLASK:
  2. DEBUG: True
  3. DEBUG_WITH_APTANA: True

然后,在应用入口( main.py ),修改如下:

  1. if __name__ == "__main__":
  2. # To allow aptana to receive errors, set use_debugger=False
  3. app = create_app(config="config.yaml")
  4. use_debugger = app.debug and not(app.config.get('DEBUG_WITH_APTANA'))
  5. app.run(use_debugger=use_debugger, debug=app.debug,
  6. use_reloader=use_debugger, host='0.0.0.0')