第七章:错误处理

感谢作者 –> 原文链接

本文翻译自The Flask Mega-Tutorial Part VII: Error Handling

这是Flask Mega-Tutorial系列的第七部分,我将告诉你如何在Flask应用中进行错误处理。

本章将暂停为microblog应用开发新功能,转而讨论处理BUG的策略,因为它们总是无处不在。为了帮助本章的演示,我故意在第六章新增的代码中遗留了一处BUG。 在继续阅读之前,看看你能不能找到它!

本章的GitHub链接为:BrowseZipDiff.

Flask中的错误处理机制

在Flask应用中爆发错误时会发生什么? 得到答案的最好的方法就是亲身体验一下。 启动应用,并确保至少有两个用户注册,以其中一个用户身份登录,打开个人主页并单击“编辑”链接。 在个人资料编辑器中,尝试将用户名更改为已经注册的另一个用户的用户名,boom!(爆炸声) 这将带来一个可怕的“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

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

值得注意的是,提供给用户的错误页面并没有提供关于错误的丰富信息,这是正确的做法。 我绝对不希望用户知道崩溃是由数据库错误引起的,或者我正在使用什么数据库,或者是我的数据库中的一些表和字段名称。 所有这些信息都应该对外保密。

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

调试模式

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

但是当你正在开发应用时,可以启用调试模式,它是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,则可以在开发应用时,每当保存文件,应用都会重新启动以加载新的代码。

自定义错误页面

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模块:


# ...
?
from app import routes, models, errors

如果在终端界面设置环境变量FLASK_DEBUG=0,然后再次出发重复用户名的BUG,你将会看到一个更加友好的错误页面。

通过电子邮件发送错误

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 = [‘[email protected]‘]

电子邮件的配置变量包括服务器和端口,启用加密连接的布尔标记以及可选的用户名和密码。 这五个配置变量来源于环境变量。 如果电子邮件服务器没有在环境中设置,那么我将禁用电子邮件功能。 电子邮件服务器端口也可以在环境变量中给出,但是如果没有设置,则使用标准端口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=‘[email protected]‘ + 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=localhostMAIL_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=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帐户。 可以阅读此处来了解具体情况,如果你担心帐户的安全性,可以创建一个辅助邮箱帐户,配置它来仅用于测试电子邮件功能,或者你可以暂时启用允许不太安全的应用程序来运行此测试,完成后恢复为默认值。

记录日志到文件中

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

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


# ...
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级别。 如果你不熟悉日志记录类别,则按照严重程度递增的顺序来认识它们就行了,分别是DEBUGINFOWARNINGERRORCRITICAL

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

修复用户名重复的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更改,无法重命名为该用户名,会再次引发数据库异常。 除了有很多服务器进程并且非常繁忙的应用之外,这种情况是不太可能的,所以现在我不会为此担心。

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

原文地址:https://www.cnblogs.com/amou/p/9427792.html

时间: 2024-10-14 06:30:03

第七章:错误处理的相关文章

【软件构造】第七章第二节 错误与异常处理

第七章第二节 错误与异常处理 本节关注:Java中错误和异常处理的典 型技术--把原理落实到代码上! Outline: Java中的错误和异常(java.lang.throwable) 异常 Runtime异常与其他异常(Exception) Checked异常和unchecked异常 checked异常的处理机制 自定义异常 Notes: ## Java中的错误和异常 [Throwable] Java.lang.throwable Throwable 类是 Java 语言中所有错误或异常的超类

软件构造 第七章第二节 错误与异常处理

第七章第二节 错误与异常处理 内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束 异常:你自己程序代码发生的,可以捕获处理 [Error] Error类描述很少发生的Java运行时系统内部的系统错误和资源耗尽情况(例如,VirtualMachineError,LinkageError). 对于内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束 Error的类型: 用户输入错误 例如:用户要求连接到语法错误的URL,网络层会投诉. 设备错误 硬件并不总是做你想做的. 输出器

ROS机器人程序设计(原书第2版)补充资料 (柒) 第七章 3D建模与仿真 urdf Gazebo V-Rep Webots Morse

ROS机器人程序设计(原书第2版)补充资料 (柒) 第七章 3D建模与仿真 urdf Gazebo V-Rep Webots Morse 书中,大部分出现hydro的地方,直接替换为indigo或jade或kinetic,即可在对应版本中使用. 提供ROS接口的3D软件比较多,本章以最典型的Gazebo介绍为主,从Player/Stage/Gazebo发展而来,现在独立的机器人仿真开发环境,目前2016年最新版本Gazebo7.1配合ROS(kinetic)使用. 补充内容:http://blo

《head first html与css、xhtml》——第六章--标准,规范,第七章--XHTML

2015-04-01 19:48:22 第六章--标准,规范 1.依据html 4.01严格的规范,内联元素必须包含在块元素中,当然,不这样做也能通过. 2.标准的网页加载速度更快,更容易在其他移动设备上运行好,同时,也容易被因视觉障碍而是用屏幕读取器的用户接受. 3.花时间去看错误的信息,你就会开始了解到他们的窍门. 4.只使用块元素填充<body>(<h1><h2>...<p><div><blockquote>..) 5.块元素在

第七章 函数

第七章  函数 7.1  函数的基础知识 要使用函数,必须完成如下工作: Ø  提供函数定义 Ø  提供函数原型 Ø  调用函数 7.1.1  函数的定义 函数总体来说可以分为两类,一类是没有返回值的,另一类是具有返回值的,两类的函数定义的格式如下 void functionName(parameterList) { statement(s) return; //可以有也可以没有 } typeName functionName(parameterList) { statement(s) retu

操作系统 庞丽萍 第七章

1. 存储管理的功能与目的是什么? 主要包括以下四个方面:(1)映射逻辑地址到物理主存地址:(2)在多用户之间分配物理主存:(3)对各个用户区的信息提供保护措施:(4)扩充逻辑主存区. 2.物理地址 VS 逻辑地址 把内存分成若干个大小相等的单元,每个单元给个编号,就是物理地址,又称为绝对地址或者实地址: 逻辑地址是用户编程序时所用的地址,又称为程序地址或者虚地址. 为了支持多道程序运行,方便用户使用:使得多用户程序共享主存, 必须要解决主存区域如何分配.各个区域内信息如何保护等问题.如果直接以

《深入理解计算机系统》读书笔记 第七章 链接

第七章链接 链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储并执行. 链接的时机 编译时,也就是在源代码被翻译成机器代码时 加载时,也就是在程序被加载器加载到存储器并执行时. 运行时,由应用程序执行. 在现代系统中,链接是由链接器自动执行的. 链接器的关键角色:使分离编译称为可能. 7.1 编译器驱动程序 驱动程序的工作:1.运行C预处理器,将C源程序(.c)翻译成一个ASCⅡ码中间文件(.i):2.运行C编译器,将.i文件翻译

课本学习笔记5:第七章 20135115臧文君

第七章 链接 注:作者:臧文君,原创作品转载请注明出处. 一.概述 1.链接(linking):是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载或被拷贝到存储器并执行. 2.链接可以执行于编译时.加载时和运行时. 3.链接器(linker):分离编译. 链接通常是由链接器执行. 二.编译器驱动程序 1.大多数编译系统提供编译驱动程序(compiler driver),它代表用户在需要时调用语言预处理器.编译器.汇编器和链接器. 例:ASCII码源文件-->可执行目标文

使用JQuery快速高效制作网页交互特效第二章到第七章

第二章 JavaScript对象 浏览器对象模型(BOM)是JavaScript的组成之一,window对象是整个BOM的核心 window对象的常用方法 prompt():显示可提示用户输入的对话框 alert():显示一个带有提示信息和一个"确定"的按钮的警示对话框 confirm():显示一个滴啊有提示信息,"确定"和"取消"按钮的对话框 close():关闭浏览器窗口 open():打开一个新的浏览器窗口,加载给定URL制定的文档 set

第七章-寻找软件的注册码

我们来寻找软件真正的注册码! 寻找软件的注册码就像你小时玩的躲猫猫一样,简单又有趣,虽然后来你会不这样觉的 好的,我们开始. 我不知道你有没有明白我前面在原理中讲的那些东西,如果没明白,我就再说一遍 软件通过你输入的用户名或者机器码什么的生成一个正确的注册码来与你输入的注册码进行比较,如果两个相同,也就是说你输入的注册码是正确的话,那软件就会完成注册.如果你输入的不正确,嘿嘿,当然就不会注册成功. 好的,现在你已经知道软件会有一个比较两个注册码的过程,这就是关键所在.一般如果你遇到的是那种明码比