带你认识 flask 错误处理

https://mp.weixin.qq.com/s?__biz=MzU2NTc1MTc5MQ==&mid=2247484688&idx=1&sn=2d929fb15d90e568595ecf2c81f7720b&chksm=fcb7bf90cbc03686593da90811693fdd612561616e7f509cf77bf5f4b05ca007666c1b6ea043#rdhtml

点击上方蓝字关注咱们python

欢迎关注个人公众号,志学Pythonweb

01sql

flask 中错误处理机制数据库

在Flask应用中爆发错误时会发生什么?获得答案的最好的方法就是亲身体验一下。启动应用,并确保至少有两个用户注册,以其中一个用户身份登陆,打开我的主页并单击“编辑”连接。在我的资料编辑器中,尝试将用户名更改成已经注册的另外一个用户的用户名,boom!(爆炸声) 这将带来一个可怕的“Internal Server Error”页面:flask

若是你查看运行应用的终端会话,将看到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

堆栈跟踪指示了BUG在何处。本应用容许用户更改用户名,但却没有验证所选的新用户名与系统中已有的其余用户有没有冲突。这个错误来自SQLAlchemy,它尝试将新的用户名写入数据库,但数据库拒绝了它,由于username列是用unique=True定义的。安全

值得注意的是,提供给用户的错误页面并无提供关于错误的丰富信息,这是正确的作法。我绝对不但愿用户知道崩溃是由数据库错误引发的,或者我正在使用什么数据库,或者是个人数据库中的一些表和字段名称。全部这些信息都应该对外保密。服务器

可是也有一些不尽人意之处。错误页面简陋不堪,与应用布局不匹配。终端上的日志不断刷新,致使重要的堆栈跟踪信息被淹没,但我却须要不断回顾它,以避免有漏网之鱼。固然,我有一个BUG须要修复。我将解决全部的这些问题,但首先,让咱们来谈谈Flask的调试模式。session

02

调试模式

你在上面看到的处理错误的方式对在生产服务器上运行的系统很是有用。若是出现错误,用户将获得一个隐晦的错误页面(尽管我打算使这个错误页面更友好),错误的重要细节在服务器进程输出或存储到日志文件中。

可是当你正在开发应用时,能够启用调试模式,它是Flask在浏览器上直接运行一个友好调试器的模式。要激活调试模式,请中止应用程序,而后设置如下环境变量:

(venv) $ export FLASK_DEBUG=1

若是你使用Microsoft Windows,记得将export替换成set。

设置环境变量FLASK_DEBUG后,重启服务。相比以前,终端上的输出信息会有所变化:

(venv) microblog2 $ 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

如今让应用再次崩溃,以在浏览器中查看交互式调试器:

该调试器容许你展开每一个堆栈框来查看相应的源代码上下文。你也能够在任意堆栈框上打开Python提示符并执行任何有效的Python表达式,例如检查变量的值。

永远不要在生产服务器上以调试模式运行Flask应用,这一点很是重要。调试器容许用户远程执行服务器中的代码,所以对于想要渗入应用或服务器的恶意用户来讲,这多是开门揖盗。做为附加的安全措施,运行在浏览器中的调试器开始被锁定,而且在第一次使用时会要求输入一个PIN码(你能够在flask run命令的输出中看到它)。

谈到调试模式的话题,我不得不提到的第二个重要的调试模式下的功能,就是重载器。这是一个很是有用的开发功能,能够在源文件被修改时自动重启应用。若是在调试模式下运行flask run,则能够在开发应用时,每当保存文件,应用都会从新启动以加载新的代码

03

自定义错误页面

Flask为应用提供了一个机制来自定义错误页面,这样用户就没必要看到简单而枯燥的默认页面。做为例子,让咱们为HTTP的404错误和500错误(两个最多见的错误页面)设置自定义错误页面。为其余错误设置页面的方式与之相同。

使用@errorhandler装饰器来声明一个自定义的错误处理器。我将把个人错误处理程序放在一个新的app/errors.py模块中。

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错误的错误处理程序应当在引起数据库错误后调用,而上面的用户名重复实际上就是这种状况。为了确保任何失败的数据库会话不会干扰模板触发的其余数据库访问,我执行会话回滚来将会话重置为干净的状态。

404错误的模板以下:

{% extends "base.html" %}

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

500错误的模板以下:

{% 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:

# ...

from app import routes, models, errors

04

经过电子邮件发送错误

Flask提供的默认错误处理机制的另外一个问题是没有通知机制,错误的堆栈跟踪只是被打印到终端,这意味着须要监视服务器进程的输出才能发现错误。在开发时,这是很是好的,可是一旦将应用部署在生产服务器上,没有人会关心输出,所以须要采用更强大的解决方案。

我认为对错误发现采起积极主动的态度是很是重要的。若是生产环境的应用发生错误,我想马上知道。因此个人第一个解决方案是配置Flask在发生错误以后当即向我发送一封电子邮件,邮件正文中包含错误堆栈跟踪的正文。

第一步,添加邮件服务器的信息到配置文件中:

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']

电子邮件的配置变量包括服务器和端口,启用加密链接的布尔标记以及可选的用户名和密码。这五个配置变量来源于环境变量。若是电子邮件服务器没有在环境中设置,那么我将禁用电子邮件功能。电子邮件服务器端口也能够在环境变量中给出,可是若是没有设置,则使用标准端口25。电子邮件服务器凭证默认不使用,但能够根据须要提供。 ADMINS配置变量是将收到错误报告的电子邮件地址列表,因此你本身的电子邮件地址应该在该列表中。

Flask使用Python的logging包来写它的日志,并且这个包已经可以经过电子邮件发送日志了。我所须要作的就是为Flask的日志对象app.logger添加一个SMTPHandler的实例:

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)

如你所见,仅当应用未以调试模式运行,且配置中存在邮件服务器时,我才会启用电子邮件日志记录器。

设置电子邮件日志记录器的步骤由于处理安全可选项而稍显繁琐。本质上,上面的代码建立了一个SMTPHandler实例,设置它的级别,以便它只报告错误及更严重级别的信息,而不是警告,常规信息或调试消息,最后将它附加到Flask的app.logger对象中。

有两种方法来测试此功能。最简单的就是使用Python的SMTP调试服务器。这是一个模拟的电子邮件服务器,它接受电子邮件,而后打印到控制台。要运行此服务器,请打开第二个终端会话并在其上运行如下命令:

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

要用这个模拟邮件服务器来测试应用,那么你将设置

MAIL_SERVER=localhost和MAIL_PORT=8025。

译者注:本段中去除了说明设置该端口须要管理员权限的部分,由于这和实际状况不符。原文以下:To test the application with this server, then you will set MAIL_SERVER=localhost and MAIL_PORT=8025. If you are on a Linux or Mac OS system, you will likely need to prefix the command with sudo, so that it can execute with administration privileges. If you are on a Windows system, you may need to open your terminal window as an administrator. Administrator rights are needed for this command because ports below 1024 are administrator-only ports. Alternatively, you can change the port to a higher port number, say 5025, and set MAIL_PORTvariable to your chosen port in the environment, and that will not require administration rights.

保持调试SMTP服务器运行并返回到第一个终端,在环境中设置export MAIL_SERVER=localhost和MAIL_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账户。能够阅读此处来了解具体状况,若是你担忧账户的安全性,能够建立一个辅助邮箱账户,配置它来仅用于测试电子邮件功能,或者你能够暂时启用容许不太安全的应用程序来运行此测试,完成后恢复为默认值。

05

记录日志到文件中

经过电子邮件来接收错误提示很是棒,但在其余场景下,有时候就有些不足了。有些错误条件既不是一个Python异常又不是重大事故,可是他们在调试的时候也是有足够用处的。为此,我将会为本应用维持一个日志文件。

为了启用另外一个基于文件类型RotatingFileHandler的日志记录器,须要以和电子邮件日志记录器相似的方式将其附加到应用的logger对象中。app/__init__.py:

# ...
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类为日志消息提供自定义格式。因为这些消息正在写入到一个文件,我但愿它们能够存储尽量多的信息。因此我使用的格式包括时间戳、日志记录级别、消息以及日志来源的源代码文件和行号。

为了使日志记录更有用,我还将应用和文件日志记录器的日志记录级别下降到INFO级别。若是你不熟悉日志记录类别,则按照严重程度递增的顺序来认识它们就好了,分别是DEBUG、INFO、WARNING、ERROR和CRITICAL。

日志文件的第一个有趣用途是,服务器每次启动时都会在日志中写入一行。当此应用在生产服务器上运行时,这些日志数据将告诉你服务器什么时候从新启动过。

06

修复用户名重复的 BUG

利用用户名重复BUG这么久, 如今时候向你展现如何修复它了。

你是否还记得,RegistrationForm已经实现了对用户名的验证,可是编辑表单的要求稍有不一样。在注册期间,我须要确保在表单中输入的用户名不存在于数据库中。在编辑我的资料表单中,我必须作一样的检查,但有一个例外。若是用户不改变原始用户名,那么验证应该容许,由于该用户名已经被分配给该用户。下面你能够看到我为这个表单实现了用户名验证:

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()方法中被校验。若是在表单中输入的用户名与原始用户名相同,那么就没有必要检查数据库是否有重复了。

为了使得新增的验证方法生效,我须要在对应视图函数中添加当前用户名到表单的username字段中:

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

如今这个BUG已经修复了,大多数状况下,之后在编辑我的资料时出现用户名重复的提交将被友好地阻止。 但这不是一个完美的解决方案,由于当两个或更多进程同时访问数据库时,这可能不起做用。假如存在验证经过的进程A和B都尝试修改用户名为同一个,但稍后进程A尝试重命名时,数据库已被进程B更改,没法重命名为该用户名,会再次引起数据库异常。 除了有不少服务器进程而且很是繁忙的应用以外,这种状况是不太可能的,因此如今我不会为此担忧。

此时,你能够尝试再次重现该错误,以了解新的表单验证方法如何防止该错误。

◆ 带你认识 flask 用户登陆◆ 带你认识 flask 中的数据库◆ 带你认识 flask web 表单◆ 带你认识 flask 的模板

相关文章
相关标签/搜索