帶你認識 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 的模板

相關文章
相關標籤/搜索