Flask項(xiàng)目異常處理機(jī)制

Date: 2021/02/07 Flask version: 1.1.2

對(duì)于前后端分離的項(xiàng)目褪迟,希望只通過(guò)JSON與前端交互仙蛉,包括異常信息也要包裝JSON格式發(fā)送給前端。要想在Flask項(xiàng)目中處理好異常擒贸,建立一套自己的異常處理機(jī)制,首先必須先知道Flask自己是如何處理異常的。進(jìn)入到flask源碼的app.py文件中养渴,可以看到所有的異常都是從werkzeug中引入的:

...
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import BadRequestKeyError
from werkzeug.exceptions import default_exceptions
from werkzeug.exceptions import HTTPException
from werkzeug.exceptions import InternalServerError
from werkzeug.exceptions import MethodNotAllowed
...

werkzeug是Flask兩大依賴之一(另一個(gè)是Jinja2),用來(lái)規(guī)定Web服務(wù)器如何與Python Web程序進(jìn)行溝通泛烙。通過(guò)源碼可以發(fā)現(xiàn)理卑,werkzeug中定義的異常類都繼承自HTTPException,下面就簡(jiǎn)單研究一下這個(gè)異潮伟保基類藐唠。通過(guò)查看源碼,發(fā)現(xiàn)HTTPException繼承自Python的Exception對(duì)象鹉究,它的構(gòu)造函數(shù)接收兩個(gè)參數(shù):description 和 response. description就是HTTPException顯示在錯(cuò)誤頁(yè)面中的異常信息宇立,而response則是一個(gè)響應(yīng)對(duì)象。這兩個(gè)參數(shù)后面會(huì)用到∽耘猓現(xiàn)在先看看它的"_call_"方法:

    def __call__(self, environ, start_response):
        """Call the exception as WSGI application.

        :param environ: the WSGI environment.
        :param start_response: the response callable provided by the WSGI
                               server.
        """
        response = self.get_response(environ)
        return response(environ, start_response)

很明顯妈嘹,當(dāng)在代碼中raise一個(gè)HTTPException時(shí),它會(huì)使用get_response()方法來(lái)生成一個(gè)response響應(yīng)對(duì)象绍妨,然后將這個(gè)response對(duì)象交給前端润脸,繼續(xù)看get_response()的內(nèi)部實(shí)現(xiàn):

    def get_response(self, environ=None):
        """Get a response object.  If one was passed to the exception
        it's returned directly.

        :param environ: the optional environ for the request.  This
                        can be used to modify the response depending
                        on how the request looked like.
        :return: a :class:`Response` object or a subclass thereof.
        """
        from .wrappers.response import Response

        if self.response is not None:
            return self.response
        if environ is not None:
            environ = _get_environ(environ)
        headers = self.get_headers(environ)
        return Response(self.get_body(environ), self.code, headers)

可以看到,get_response()方法考慮了兩種情況:

  1. 如果self.response對(duì)象不為空他去,它就直接返回這個(gè)response對(duì)象作為異常響應(yīng)津函;
  2. 如果self.response對(duì)象為空,它會(huì)調(diào)用get_headers()方法和get_body()方法來(lái)生成一個(gè)response對(duì)象

關(guān)于get_headers()和get_body()方法孤页,看一下它的源碼就很容易理解了:

    def get_body(self, environ=None):
        """Get the HTML body."""
        return text_type(
            (
                u'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n'
                u"<title>%(code)s %(name)s</title>\n"
                u"<h1>%(name)s</h1>\n"
                u"%(description)s\n"
            )
            % {
                "code": self.code,
                "name": escape(self.name),
                "description": self.get_description(environ),
            }
        )

    def get_headers(self, environ=None):
        """Get a list of headers."""
        return [("Content-Type", "text/html; charset=utf-8")]

HTTPException通過(guò)get_headers()生成頭部信息尔苦,通過(guò)get_body()生成具體內(nèi)容,我們?cè)跇?gòu)造函數(shù)中傳入的description參數(shù)就是在這里傳入get_body()中,兩者配合定義了一個(gè)HTML頁(yè)面允坚。這里的關(guān)鍵點(diǎn)是get_heades()方法將響應(yīng)對(duì)象的“Content-Type”參數(shù)設(shè)為了“text/html”格式魂那,這就是為什么它會(huì)返還給前端一個(gè)HTML頁(yè)面的原因。顯然稠项,這里只要將get_headers()中的“Content-Type”改寫為“application/json”涯雅,然后再改寫get_body()中的內(nèi)容,就能讓它返回JSON格式的數(shù)據(jù)了展运。

當(dāng)然活逆,以上只是第一種方法。再回到之前的get_response()方法中拗胜,它里面的self.response對(duì)象就是開(kāi)頭在構(gòu)造函數(shù)中傳入的那個(gè)response參數(shù)蔗候,就是說(shuō),只要我們定義一個(gè)JSON格式的response對(duì)象傳給HTTPException的構(gòu)造函數(shù)就能達(dá)到我們想要的效果了埂软。

綜上所述锈遥,我們有兩種方法來(lái)實(shí)現(xiàn)我們的目的:

  1. 重寫get_headers()和get_body()方法;
  2. 傳入一個(gè)JSON格式的response對(duì)象勘畔;

這兩種方法都可以通過(guò)定義一個(gè)繼承自HTTPException的子類來(lái)實(shí)現(xiàn)所灸,以下我將分別實(shí)現(xiàn)這兩種方法。

自定義異常處理類

方法一:重寫get_headers()和get_body()方法
新定義一個(gè)APIException炫七,使其繼承自HTTPException爬立,代碼如下:

class APIException(HTTPException):
    code = 500  # http status code
    error_code = 10999  # 項(xiàng)目?jī)?nèi)部使用的接口狀態(tài)碼
    message = 'Server Internal Error'

    def __init__(self, code=None, message=None, error_code=None):
        if code is not None:
            self.code = code
        if message is not None:
            self.message = message
        if error_code is not None:
            self.error_code = error_code

        super(APIException, self).__init__(self.message, None)

    def get_body(self, environ=None):
        body = dict(
            code=self.error_code,
            message=self.message,
            data=None,
            request=request.method + ' ' + self.get_url_without_param())
        return json.dumps(body)

    def get_headers(self, environ=None):
        return [('Content-Type', 'application/json')]

    @staticmethod
    def get_url_without_param():
        full_url = str(request.full_path)
        return full_url.split('?')[0]

方法二:傳入一個(gè)JSON格式的response對(duì)象
直接上代碼,如下:

class APIException(HTTPException):
    code = 500
    error_code = 10999
    message = 'Server Internal Error'
    
    def __init__(self, code=None, message=None, error_code=None):
        if code is not None:
            self.code = code
        if message is not None:
            self.message = message
        if error_code is not None:
            self.error_code = error_code

        super(APIException, self).__init__(response=self.__make_response())

    def __make_response(self):
        r = {
            'code': self.error_code,
            'message': self.message,
            'data': None
        }
        responese = Response(json.dumps(r), mimetype='application/json')
        return responese

定義場(chǎng)景錯(cuò)誤類

有了上面我們改寫好的APIException類万哪,我們就可以自由的定義各種狀態(tài)碼的錯(cuò)誤以及對(duì)應(yīng)的錯(cuò)誤信息侠驯,然后在合適的位置拋出即可,如下:

...
class ParameterError(APIException):
    code = 400
    error_code = APIStatusCode.PARAMETER_ERROR.code     # APIStatusCode是我項(xiàng)目中定義的接口狀態(tài)碼枚舉類
    message = APIStatusCode.PARAMETER_ERROR.message


class InvalidToken(APIException):
    code = 401
    error_code = APIStatusCode.Invalid_Token.code
    message = APIStatusCode.Invalid_Token.message

...

接下來(lái)做一個(gè)簡(jiǎn)單的測(cè)試壤圃,在視圖函數(shù)中raise ParameterError, 然后使用curl命令請(qǐng)求接口:

@api.route('/invoke', methods=['GET', 'POST'])
def invoke():
    raise ParameterError()
curl http://127.0.0.1:5008/api/v1/mock/invoke
{"code": 400, "message": "Parameter Error", "data": null, "request": "GET /api/v1/mock/invoke"}

可以看到結(jié)果是完全符合預(yù)期的陵霉。這個(gè)例子充分體現(xiàn)了Flask的靈活性琅轧,這也是我喜愛(ài)Flask最重要的原因伍绳。同時(shí),也說(shuō)明HTTPException在設(shè)計(jì)時(shí)就已經(jīng)考慮好了開(kāi)發(fā)者對(duì)它的重構(gòu)乍桂,使我們能方便實(shí)現(xiàn)自己的異常處理方式冲杀。

注冊(cè)全局錯(cuò)誤處理函數(shù)

盡管可以在認(rèn)為可能出錯(cuò)的所有地方,定義自己的錯(cuò)誤類然后拋出睹酌,但是也不是所有的異常都是可以提前預(yù)知的权谁。比如我們接收前端傳來(lái)的參數(shù),參數(shù)類型或取值范圍不正確憋沿,這些我們可以預(yù)知并處理好旺芽,但是如果是邏輯處理中出現(xiàn)了問(wèn)題,這些不是我們程序可以控制并處理的。所以光有自定義錯(cuò)誤類還不夠采章,我們還需要在全局捕獲異常來(lái)判斷运嗜,利用AOP思想。

def register_errors(app):
    @app.errorhandler(Exception)
    def framework_error(e):
        if isinstance(e, APIException):  # 手動(dòng)觸發(fā)的異常
            return e
        elif isinstance(e, HTTPException):  # 代碼異常
            return APIException(e.code, e.description, None)
        else:
            if current_app.config['DEBUG']:
                raise e
            else:
                return ServerError()

然后再在工廠函數(shù)中進(jìn)行注冊(cè):

def create_app(config_name=None):
    """Flask Application Factory Function"""
    app = Flask(__name__)
    ....
    register_errors(app) 
    ....

關(guān)于flask的異常處理悯舟,以上就是我目前學(xué)習(xí)到的一些經(jīng)驗(yàn)技巧担租,如有錯(cuò)誤歡迎指出,,后續(xù)會(huì)不斷更新抵怎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末奋救,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子反惕,更是在濱河造成了極大的恐慌尝艘,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件承璃,死亡現(xiàn)場(chǎng)離奇詭異利耍,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)盔粹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門隘梨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人舷嗡,你說(shuō)我怎么就攤上這事轴猎。” “怎么了进萄?”我有些...
    開(kāi)封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵捻脖,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我中鼠,道長(zhǎng)可婶,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任援雇,我火速辦了婚禮矛渴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惫搏。我一直安慰自己具温,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布筐赔。 她就那樣靜靜地躺著铣猩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪茴丰。 梳的紋絲不亂的頭發(fā)上达皿,一...
    開(kāi)封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天天吓,我揣著相機(jī)與錄音,去河邊找鬼峦椰。 笑死失仁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的们何。 我是一名探鬼主播萄焦,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼冤竹!你這毒婦竟也來(lái)了拂封?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鹦蠕,失蹤者是張志新(化名)和其女友劉穎冒签,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體钟病,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萧恕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肠阱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片票唆。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖屹徘,靈堂內(nèi)的尸體忽然破棺而出走趋,到底是詐尸還是另有隱情,我是刑警寧澤噪伊,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布簿煌,位于F島的核電站,受9級(jí)特大地震影響鉴吹,放射性物質(zhì)發(fā)生泄漏姨伟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一豆励、第九天 我趴在偏房一處隱蔽的房頂上張望夺荒。 院中可真熱鬧,春花似錦肆糕、人聲如沸般堆。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至私沮,卻和暖如春始赎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工造垛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留魔招,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓五辽,卻偏偏與公主長(zhǎng)得像办斑,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子杆逗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

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