Flask Mega-Tutorial 中文教程 V2.0 第7章:错误处理

最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。

这是Flask Mega-Tutorial系列的第七章,其中我将告诉您如何在Flask应用程序中进行错误处理。

供您参考,以下是本系列文章的列表。

在本章中,我暂时不添加新功能到我的microblog应用程序中,而是讨论一些错误处理的策略,这些错误总是会出现在任何软件项目中。为了帮助说明这个主题,我故意在第6章中添加的代码中遗留了一个错误。在继续阅读之前,看看你是否能找到它!

本章的GitHub链接是:BrowseZipDiff

 


Flask中的错误处理

在Flask中发生错误时会发生什么?找出答案的最佳方法是亲自体验。启动应用程序,并确保至少有两个用户注册。以其中一个用户身份登录,打开配置文件页面并单击“编辑”链接。在配置文件编辑器中,尝试将用户名更改为另一个已注册用户的用户名,然后boom!这将带来一个可怕的“Internal Server Error”页面:

ch07-Internal-Server-Error

如果查看运行应用程序的终端会话,您将看到错误的堆栈跟踪(stack trace )。堆栈跟踪在调试错误时非常有用,因为它们显示该堆栈中的调用序列,一直到产生错误的行:

(venv) $ flask run
 * Serving Flask app "microblog"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
[2017-09-14 22:40:02,027] ERROR in app: Exception on /edit_profile [POST]
Traceback (most recent call last):
  File "/home/miguel/microblog/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1182, in _execute_context
    context)
  File "/home/miguel/microblog/venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 470, in do_execute
    cursor.execute(statement, parameters)
sqlite3.IntegrityError: UNIQUE constraint failed: user.username

堆栈跟踪指出错误是什么。应用程序允许用户更改用户名,但不会验证更改的新用户名是否与数据库中已有的其他用户发生冲突。该错误来自SQLAlchemy,它尝试将新用户名写入数据库,但数据库拒绝它,因为username被定义了unique=True

注意,渲染给用户的错误页面没有提供有关错误的大量信息,这很好。我绝对不希望用户知道崩溃的原因是,我正在使用的数据库发生错误。所有这些错误信息都应该保留在内部。

有一些事情并不理想。我有一个非常难看的与网站布局不匹配的错误页面。还有一个重要的事情,需要我不断观察堆栈跟踪被转储到终端上的错误信息,以确保我不会错过任何错误。当然,我还有一个bug要修复。

我将解决所有这些问题,但首先,我们来谈谈Flask的调试模式

调试模式

上面处理错误的方式对于在生产服务器上运行的系统来说非常有用。如果出现错误,用户会得到一个模糊的错误页面(虽然我将使这个错误页面更好),并且错误的重要细节在服务器进程输出显示或存进一个日志文件中。

但是在开发应用程序时,可以启用调试模式,在这个模式下,Flask可以直接在浏览器上输出非常好的调试器。要激活调试模式,请先停止应用程序,然后设置以下环境变量:

(venv) $ export FLASK_DEBUG=1

如果您使用的是Microsoft Windows,请记住使用set而不是export

设置后FLASK_DEBUG,重新启动服务器。终端上的输出将与以前的略有不同:

(venv) $ flask run
 * Serving Flask app "microblog"
 * Forcing debug mode on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 177-562-960

译者注:Debugger pin code 在使用页面调试时需输入确认,详见下

现在让应用程序再次崩溃,以便在浏览器中查看交互式调试器:

ch07-debugger

调试器允许您展开每个堆栈帧并查看相应的源代码。您还可以在任何调试页面上打开Python并执行任何有效的Python语句,例如检查变量的值。

在生产服务器上,千万不要使用调试模式运行Flask,这非常重要。调试器允许用户远程执行服务器中的代码,因此对于想要渗透您的应用程序或服务器的恶意用户来说,这可能是一份的意外礼物。作为额外的安全措施,在浏览器中运行的调试器开始锁定,并且在首次使用时将询问PIN号,您可以在flask run命令的输出中看到PIN号。

译者注:打开调试器如下图

调试模式启用的第二个重要功能,即重新加载器。这是一个非常有用的开发功能,可在修改源文件时自动重新启动应用程序。如果在调试模式下运行flask run,则会在每次保存文件时,应用程序都将重新启动以获取新代码。

自定义错误页面

Flask为应用程序提供了一种自定义错误页面的机制,这样您的用户就不必看到乏味的默认页面。譬如,我们可以为HTTP 404错误和500错误这两个最常见的自定义错误页面。其他错误自定义页面的工作方式与此相同。

使用@errorhandler装饰器,来声明自定义错误处理程序。

我将把错误处理程序放在一个新的app/errors.py模块中。

# app/errors.py: Custom error handlers

from flask import render_template
from app import app, db

@app.errorhandler(404)
def not_found_error(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()
    return render_template('500.html'), 500

错误函数与视图函数的工作方式非常相似。对于这两个错误,我将返回各自模板的内容。请注意,两个函数都在模板后面返回第二个值,即错误代码编号。对于我到目前为止创建的所有视图函数,我不需要添加第二个返回值,因为默认值为200(成功响应的状态代码)是我想要的。在这种情况下,这些是错误页面,所以我希望响应的状态代码能够反映出来。

在数据库报错之后,实际上是上面的用户名重复,我们可以调用500错误的错误处理程序。在数据库被一个服务器错误出发之后,数据库会话会处于一个不正常的状态,因此我们必须把会话回滚都正常工作状态,即渲染500错误页之前。这将会话重置为干净状态,以确保不会干扰其它任何数据库访问。

以下是404错误的模板:

app/templates/404.html: Not found error template

{% extends "base.html" %}

{% block content %}
    <h1>File Not Found</h1>
    <p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}

这是500错误的模板:

app/templates/500.html: Internal server error template

{% extends "base.html" %}

{% block content %}
    <h1>An unexpected error has occurred</h1>
    <p>The administrator has been notified. Sorry for the inconvenience!</p>
    <p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}

上面两个模板我们继续使用base.html布局,这可以让错误页面和应用程序的外观保持统一。

要使用这些错误处理程序,可以通过在Flask创建应用程序实例后,导入新的app/errors.py模块来注册它们:

# app/__init__.py: Import error handlers

# ...

from app import routes, models, errors

如果您在终端会话中设置FLASK_DEBUG=0,然后再次触发重复的用户名错误,您将看到一个稍微友好的错误页面。

 

通过电子邮件发送错误

Flask提供的默认错误处理的另一个问题是没有通知,错误的堆栈跟踪被打印到终端,这意味着需要监视服务器进程的输出显示才能发现错误。当您在开发期间运行应用程序时,这非常好,但是一旦将应用程序部署在生产服务器上,就没有人会查看输出,因此需要采用更强大的解决方案。

我认为对错误采取积极主动的态度非常重要。如果应用程序的生产版本发生错误,我想马上知道。因此,我的第一个解决方案是将Flask配置为在发生错误后立即向我发送电子邮件,并在电子邮件正文中显示错误的堆栈跟踪。

第一步是将电子邮件服务器详细信息添加到配置文件中:

# config.py: Email configuration

class Config(object):
    # ...
    MAIL_SERVER = os.environ.get('MAIL_SERVER')
    MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    ADMINS = ['your-email@example.com']

电子邮件的配置变量包括邮件服务器、端口,启用加密连接的布尔标志以及可选的用户名和密码。五个配置变量来自它们的对应环境变量。如果未在环境变量中设置电子邮件服务器MAIL_SERVER,我将使用它作为需要禁用电子邮件发送错误的标志。电子邮件服务器端口MAIL_PORT也可以在环境变量中给出,如果未设置,则使用标准端口25。默认情况下不使用电子邮件服务器凭据MAIL_USE_TLS,但也可以根据需要提供。配置ADMINS变量是一个能收到错误报告的电子邮件地址的列表,确保你的电子邮箱在其中。

Flask使用Python的logging包来编写日志,这个包可以通过电子邮件发送日志。我仅需将一个SMTPHandler实例添加到Flask logger对象中,即app.logger

# app/__init__.py: Log errors by email

import logging
from logging.handlers import SMTPHandler

# ...

if not app.debug:
    if app.config['MAIL_SERVER']:
        auth = None
        if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
            auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
        secure = None
        if app.config['MAIL_USE_TLS']:
            secure = ()
        mail_handler = SMTPHandler(
            mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
            fromaddr='no-reply@' + app.config['MAIL_SERVER'],
            toaddrs=app.config['ADMINS'], subject='Microblog Failure',
            credentials=auth, secure=secure)
        mail_handler.setLevel(logging.ERROR)
        app.logger.addHandler(mail_handler)

正如上面代码,我只打算在没有打开调试模式(app.debug = False)以及配置了MAIL_SERVER参数存在时,启用电子邮件logging。

由于必须处理许多电子邮件服务器中存在的可选安全选项,因此设置电子邮件logging有点单调乏味。但实际上,上面的代码创建了一个SMTPHandler实例,设置了它的级别,使它只报告错误而不是警告、信息或调试消息,最后将它从Flask 附加到app.logger对象。

有两种方法可以测试此功能。最简单的方法是使用Python的SMTP调试服务器。这是一个虚假的电子邮件服务器,它接受电子邮件,但不是发送它们,而是将它们打印到控制台。要运行此服务器,请打开第二个终端会话并对其运行以下命令:

(venv) $ python -m smtpd -n -c DebuggingServer localhost:8025

让调试SMTP服务器保持运行,然后返回到您的第一个终端,在环境中设置export MAIL_SERVER=localhostMAIL_PORT=8025(如果您使用的是Microsoft Windows ,则使用set而不是export)。确保FLASK_DEBUG变量设置为0或根本不设置,因为应用程序不会以调试模式发送电子邮件。

运行应用程序并再次触发SQLAlchemy错误,以查看运行虚假电子邮件服务器的终端会话如何显示包含错误的完整堆栈跟踪的电子邮件。

此功能的第二种测试方法是配置真实的电子邮件服务器。以下是使用Gmail帐户的电子邮件服务器的配置:

export MAIL_SERVER=smtp.googlemail.com
export MAIL_PORT=587
export MAIL_USE_TLS=1
export MAIL_USERNAME=<your-gmail-username>
export MAIL_PASSWORD=<your-gmail-password>

如果您使用的是Microsoft Windows,请记住使用set而不是export

Gmail帐户中的安全功能可能会阻止应用程序通过它发送电子邮件,除非您明确允许“安全性较低的应用”访问您的Gmail帐户。您可以在此处阅读了解更多内容。如果您担心帐户的安全性,可以创建仅为测试电子邮件而配置的辅助帐户,或者您可以暂时启用安全性较低的应用程序来运行此测试,然后再还原到默认值。

记录到文件

通过电子邮件接收错误很好,但有时这还不够。有一些失败并不会以异常结束而且也不是主要问题,然而我们可能想要在日志找那个追踪他们以便做一些调试。出于这个原因,我还要为应用程序维护一个日志文件。

要启用基于文件的日志,需要另一个处理程序(RotatingFileHandler),它以与电子邮件处理程序类似的方式附加到Flask logger对象

# app/__init__.py: Email configuration

# ...
from logging.handlers import RotatingFileHandler
import os

# ...

if not app.debug:
    # ...

    if not os.path.exists('logs'):
        os.mkdir('logs')
    file_handler = RotatingFileHandler('logs/microblog.log', maxBytes=10240,
                                       backupCount=10)
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
    file_handler.setLevel(logging.INFO)
    app.logger.addHandler(file_handler)

    app.logger.setLevel(logging.INFO)
    app.logger.info('Microblog startup')

我将在logs目录中写入microblog.log日志文件,如果它尚不存在,我将创建该日志文件。

RotatingFileHandler类是很好用,因为它轮转日志,确保当应用程序运行很长一段时间的日志文件不会变得太大。在这种情况下,我将日志文件的大小限制为10KB,并且我将最后十个日志文件保留为备份。

logging.Formatter类提供自定义格式的日志消息。由于这些消息将转到文件,我希望它们拥有尽可能多的信息。所以我使用的格式包括时间戳,日志记录级别,日志消息以及堆栈跟踪的文件和行号。

为了使日志记录更有用,我还将flask logger和文件记录器中的日志记录级别降低到INFO。如果您不熟悉日志记录类别,它们按严重程度递增分别是DEBUGINFOWARNING、ERRORCRITICAL

作为日志文件的第一个有趣用途,服务器每次启动时都会在日志中写入一行。当此应用程序在生产服务器上运行时,这些日志条目将告诉您服务器何时重新启动。

修复用户名重复错误

用户名重复bug已经存在太长时间了。既然我已经向您展示了应用程序如何处理这类错误,我们现在可以修复它。

如果您还记得,RegistrationForm已经实现了用户名的验证,但编辑表单的要求略有不同。在注册过程中,我需要确保数据库中不存在注册的用户名。在编辑配置文件表单上,我必须也执行相同的检查。但有一个例外,如果用户保持用户名不变,则验证应允许,因为此用户名已分配给该用户。下面我将演示如何为此表单实现用户名验证:

# app/forms.py: Validate username in edit profile form.

class EditProfileForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
    submit = SubmitField('Submit')

    def __init__(self, original_username, *args, **kwargs):
        super(EditProfileForm, self).__init__(*args, **kwargs)
        self.original_username = original_username

    def validate_username(self, username):
        if username.data != self.original_username:
            user = User.query.filter_by(username=self.username.data).first()
            if user is not None:
                raise ValidationError('Please use a different username.')

用自定义验证方法来实现这个验证,但是需要重载一个构造函数,它接受原始用户名作为参数。此用户名保存为实例变量,并在validate_username()方法中进行检查。如果在表单中输入的用户名与原始用户名相同,则没有理由检查数据库是否有重复项。

要使用这个新的验证方法,我需要在视图函数中添加原始用户名参数,并创建新表单对象:

# app/routes.py: Validate username in edit profile form.

@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    form = EditProfileForm(current_user.username)
    # ...

现在修复了错误,并且在大多数情况下将防止编辑配置文件表单中的重复项。但这并不是一个完美的解决方案,因为当两个或多个进程同时访问数据库时它可能不起作用。在这种情况下,竞争条件可能导致验证通过,但是稍后当尝试重命名时,数据库已被另一个进程更改,并且无法重命名用户。除了非常繁忙的具有大量服务器进程的应用程序之外,这有点不太可能,所以我现在不用担心它。

此时,您可以尝试再次重现错误,以查看新表单验证方法如何防止它。


原文链接:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vii-error-handling

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值