Flask源碼之異常處理-兼論前后端分離場(chǎng)景下的接口格式問(wèn)題(五)

引言

問(wèn)題提出

先看下面一段代碼

from flask import Flask, jsonify

flask_app = Flask(__name__)


@flask_app.route('/', endpoint="11")
def hello_world():
    a = 1 / 0
    return jsonify(
        code=0,
        msg="success",
        data=["hello,world!"]
    )

if __name__ == '__main__':
    flask_app.run()

這段代碼有一個(gè)很顯然的未捕捉的異常限匣,就是1/0

再看flask在生產(chǎn)環(huán)境中返回了什么惯疙,一個(gè)狀態(tài)是500、內(nèi)容是html的返回

image-20210105234225278

然而在用json格式交互的前后端分離場(chǎng)景下,前端希望后端仍然返回json格式的數(shù)據(jù)瓶摆,而不是html

再考慮到可能以后還有安卓和ios凹蜂,人家可能都不需要html

所以我們需要在后端服務(wù)出現(xiàn)未捕獲的異常時(shí)候馍驯,返回自定義的json格式數(shù)據(jù)

修改代碼如下

from flask import Flask, jsonify

flask_app = Flask(__name__)


@flask_app.route('/', endpoint="11")
def hello_world():
    a = 1 / 0
    return jsonify(
        code=0,
        msg="success",
        data=["hello,world!"]
    )


@flask_app.errorhandler(500)
def handle_500(e):
    # 可能還要記錄一下自定義的日志
    # 也可能還需要回滾一下數(shù)據(jù)庫(kù)操作
    return jsonify(
        code=-1,
        msg="unknown error"
    )


if __name__ == '__main__':
    flask_app.run()

重啟服務(wù)

image-20210105235636806

這樣,我們的前端玛痊、ios和安卓只需要再一個(gè)全局的位置汰瘫,判斷一下,如果code等于 -1擂煞,就展示unknown error或者自己換個(gè)名字 服務(wù)器未知異常就好了

這里我們用到了 flask提供的 errorhandler

為什么建議用errorhandler

有的人可能會(huì)在沒(méi)有看文檔的情況下混弥,寫(xiě)一個(gè)裝飾器去裝飾視圖函數(shù),來(lái)捕捉未知異常对省,像這樣

from flask import Flask, jsonify

flask_app = Flask(__name__)


def handle_500(func):
    def wrapper(*args, **kw):
        try:
            rv = func(*args, **kw)
        except Exception as e:
            # 可能還要記錄一下自定義的日志
            # 可能還需要回滾數(shù)據(jù)庫(kù)操作
            rv = {
                "code": -1,
                "msg": "unknown error"
            }
        return rv

    return wrapper


@flask_app.route('/', endpoint="11")
@handle_500
def hello_world():
    a = 1 / 0
    return jsonify(
        code=0,
        msg="success",
        data=["hello,world!"]
    )


# @flask_app.errorhandler(500)
# def handle_500(e):
#     # 可能還要記錄一下自定義的日志
#     return jsonify(
#         code=-1,
#         msg="unknown error"
#     )


if __name__ == '__main__':
    flask_app.run()

這樣寫(xiě)我個(gè)人不太建議:

  1. 官方文檔建議用errorhandler的處理方式蝗拿,最好用這種
  2. handle_500這個(gè)裝飾器要在@app.route這個(gè)裝飾器下面才有作用
  3. 如果你用的是函數(shù)視圖而不是類(lèi)視圖,那么你每個(gè)函數(shù)都要加這樣一個(gè)裝飾器官辽,產(chǎn)生重復(fù)代碼蛹磺,還可能會(huì)遺忘。如果你用類(lèi)視圖同仆,可以寫(xiě)一個(gè)被裝飾的基類(lèi)
  4. 這個(gè)裝飾器只能捕捉函數(shù)視圖或者類(lèi)視圖中的異常萤捆,我們?cè)陂_(kāi)發(fā)中還有 @app.before_request等鉤子函數(shù),里面也會(huì)出現(xiàn)異常俗批,這個(gè)裝飾器無(wú)法捕捉俗或,但 errorhandler可以
  5. 這樣更加解耦

2345的問(wèn)題errorhandler都可以解決

原理

下面是程序執(zhí)行的方法調(diào)用棧,基本原理就是異常在源碼中已經(jīng)被catch了然后檢查Flask對(duì)象有沒(méi)有對(duì)應(yīng)的handler可以處理岁忘,沒(méi)有就上拋辛慰,一直拋

image-20210107155113928

源碼實(shí)現(xiàn)

這里用斷點(diǎn)調(diào)試就很簡(jiǎn)單了

先在 full_dispatch_request 打斷點(diǎn),ctrl+鼠標(biāo)左鍵進(jìn)入方法內(nèi)部

image-20210106003550310

full_dispatch_request方法內(nèi)部打斷點(diǎn)干像,注意要打兩個(gè)斷點(diǎn)帅腌,然后按f9讓程序走到這個(gè)斷點(diǎn)

image-20210106004015653

進(jìn)入dispatch_request內(nèi)部驰弄,在self.view_functions[rule.endpoint](**req.view_args)處打斷點(diǎn)

image-20210106003750673

self.view_functions[rule.endpoint](**req.view_args),這一步其實(shí)就是在執(zhí)行 hello_world()

我們知道 hello_world這個(gè)函數(shù)會(huì)拋出一個(gè) 零不能被除的異常

那這個(gè)異常會(huì)被捕捉嗎速客?

繼續(xù)往前調(diào)試戚篙,我們會(huì)回到上一層(因?yàn)槲覀冊(cè)谥?code>full_dispatch_request內(nèi)部打了兩個(gè)斷點(diǎn))

image-20210106004158333

零不能被除異常在上一層被捕捉到了,這個(gè)e就是我們的異常

我們?cè)谶@個(gè)地方停留一會(huì)兒溺职,我們發(fā)現(xiàn)這個(gè)try內(nèi)部除了 dispatch_request 岔擂,還有 rv = self.preprocess_request()這一句,看名字也知道是預(yù)處理浪耘,什么預(yù)處理呢乱灵?沒(méi)錯(cuò)就是之前提到的 @app.before_request裝飾的鉤子函數(shù),由此可見(jiàn)七冲,鉤子函數(shù)的異常也會(huì)被捕捉到

繼續(xù)看except之后的代碼

那么 rv = self.handle_user_exception(e)會(huì)幫我們處理這個(gè) division by zero異常嗎痛倚?

其實(shí)也不會(huì),他會(huì)把異常繼續(xù)往上拋癞埠,我們稍后再講他的作用

點(diǎn)擊調(diào)用棧的這個(gè)地方状原,我們要回到上一層,繼續(xù)打上一個(gè)斷點(diǎn)苗踪,注意看圖中 Frames的位置,選擇箭頭指向的上一層

image-20210106004705665

error=e處打斷點(diǎn)

image-20210106005121465

估計(jì)你也知道了削锰,零不能被除異常被 handle_user_exception(e)又拋了出來(lái)通铲,在上一層的 wsgi_app方法中被捕捉到了,并且交給 handle_exception來(lái)處理

handle_exception做的事情也簡(jiǎn)單

image-20210106005403417

先打日志器贩,把出錯(cuò)類(lèi)型 出錯(cuò)值 出錯(cuò)調(diào)用棧全打出來(lái)

然后再準(zhǔn)備報(bào) InternalServerError也就是狀態(tài)碼是500flask服務(wù)器異常

但是在正式返回之前颅夺,會(huì)先看一下你有沒(méi)有 處理500錯(cuò)誤的handler,而我們是有的,于是調(diào)用你的handle_500函數(shù)處理服務(wù)器異常

注意這句 if self.propagate_exception蛹稍,self.propagate_exception這個(gè)屬性是為了決定是否傳播你的異常吧黄,在開(kāi)發(fā)和測(cè)試環(huán)境下,這個(gè)為真唆姐,異常會(huì)被繼續(xù)拋到上層拗慨,所以我們寫(xiě)的handler會(huì)不生效,因此奉芦,為了看到我們的500錯(cuò)誤處理器生效赵抢,記得在生產(chǎn)環(huán)境查看

image-20210106005724883

選中 handler右鍵在彈出菜單中選擇evaluate,可以發(fā)現(xiàn)這個(gè)handler就是我們定義的handle_500函數(shù),handle_500返回的就是我們自定義的json格式

handle_user_exception處理什么異常

image-20210106201113736

比較容易看出声功,這個(gè)方法主要是處理 HTTPException和用戶(hù)自定義的非500的異常

看個(gè)例子

from flask import Flask, jsonify

flask_app = Flask(__name__)


class ValidationError(ValueError):
    pass


@flask_app.route('/', endpoint="11", methods=["GET", "POST"])
def hello_world():
    raise ValidationError("參數(shù)驗(yàn)證失敗")


@flask_app.errorhandler(ValidationError)
def validation_error(e):
    return jsonify(
        code=1,
        msg=e.args
    )


if __name__ == '__main__':
    flask_app.run()

例如你的代碼里面有多處需要拋出和捕獲 ValidationError這個(gè)錯(cuò)誤烦却,進(jìn)一步說(shuō),在orm層會(huì)拋出這個(gè)異常先巴,view層捕捉其爵,那么寫(xiě)多次try不妨考慮用這種全局異常注冊(cè)的方式

現(xiàn)在我們看看源碼怎么處理的

handle_user_exception內(nèi)部打上斷點(diǎn)

image-20210106224104610
image-20210106224222700

handler就是我們的 validation_error函數(shù)冒冬,最后返回的實(shí)際上是 validation_error函數(shù)的執(zhí)行結(jié)果

后端api格式

我們都知道rest是一種風(fēng)格的api

他用http 碼來(lái)表示狀態(tài),但實(shí)際中有時(shí)候是不夠用的摩渺,或者前端简烤、ios和安卓都希望不用http的什么400狀態(tài)碼,

而需要在json數(shù)據(jù)中再定義自己的狀態(tài)碼证逻,http狀態(tài)碼統(tǒng)一200

像這樣

{
    "code":0,
    "msg":"success",
    "data":[]
}

這種常見(jiàn)于國(guó)內(nèi)比較大型的項(xiàng)目中

到底是采用rest風(fēng)格的api還是自定義狀態(tài)碼乐埠,爭(zhēng)論很多,具體還是看公司要求

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末囚企,一起剝皮案震驚了整個(gè)濱河市丈咐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌龙宏,老刑警劉巖棵逊,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異银酗,居然都是意外死亡辆影,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)黍特,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蛙讥,“玉大人,你說(shuō)我怎么就攤上這事灭衷〈温” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵翔曲,是天一觀(guān)的道長(zhǎng)迫像。 經(jīng)常有香客問(wèn)我,道長(zhǎng)瞳遍,這世上最難降的妖魔是什么闻妓? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮掠械,結(jié)果婚禮上由缆,老公的妹妹穿的比我還像新娘。我一直安慰自己份蝴,他們只是感情好犁功,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著婚夫,像睡著了一般浸卦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上案糙,一...
    開(kāi)封第一講書(shū)人閱讀 49,784評(píng)論 1 290
  • 那天限嫌,我揣著相機(jī)與錄音靴庆,去河邊找鬼。 笑死怒医,一個(gè)胖子當(dāng)著我的面吹牛炉抒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播稚叹,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼焰薄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了扒袖?” 一聲冷哼從身側(cè)響起塞茅,我...
    開(kāi)封第一講書(shū)人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎季率,沒(méi)想到半個(gè)月后野瘦,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡飒泻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年鞭光,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泞遗。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡惰许,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出史辙,到底是詐尸還是另有隱情啡省,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布髓霞,位于F島的核電站,受9級(jí)特大地震影響畦戒,放射性物質(zhì)發(fā)生泄漏方库。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一障斋、第九天 我趴在偏房一處隱蔽的房頂上張望纵潦。 院中可真熱鬧,春花似錦垃环、人聲如沸邀层。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)寥院。三九已至,卻和暖如春涛目,著一層夾襖步出監(jiān)牢的瞬間秸谢,已是汗流浹背凛澎。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留估蹄,地道東北人塑煎。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像臭蚁,于是被迫代替她去往敵國(guó)和親最铁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容