Flask項目異常處理機制

Date: 2021/02/07 Flask version: 1.1.2

對於前後端分離的項目,希望只通過JSON與前端交互,包括異常信息也要包裝JSON格式發送給前端。要想在Flask項目中處理好異常,建立一套自己的異常處理機制,首先必須先知道Flask自己是如何處理異常的。進入到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兩大依賴之一(另一個是Jinja2),用來規定Web服務器如何與Python Web程序進行溝通。通過源碼可以發現,werkzeug中定義的異常類都繼承自HTTPException,下面就簡單研究一下這個異常基類。通過查看源碼,發現HTTPException繼承自Python的Exception對象,它的構造函數接收兩個參數:description 和 response. description就是HTTPException顯示在錯誤頁面中的異常信息,而response則是一個響應對象。這兩個參數後面會用到。現在先看看它的"_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)

很明顯,當在代碼中raise一個HTTPException時,它會使用get_response()方法來生成一個response響應對象,然後將這個response對象交給前端,繼續看get_response()的內部實現:

    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對象不爲空,它就直接返回這個response對象作爲異常響應;
  2. 如果self.response對象爲空,它會調用get_headers()方法和get_body()方法來生成一個response對象

關於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通過get_headers()生成頭部信息,通過get_body()生成具體內容,我們在構造函數中傳入的description參數就是在這裏傳入get_body()中,兩者配合定義了一個HTML頁面。這裏的關鍵點是get_heades()方法將響應對象的“Content-Type”參數設爲了“text/html”格式,這就是爲什麼它會返還給前端一個HTML頁面的原因。顯然,這裏只要將get_headers()中的“Content-Type”改寫爲“application/json”,然後再改寫get_body()中的內容,就能讓它返回JSON格式的數據了。

當然,以上只是第一種方法。再回到之前的get_response()方法中,它裏面的self.response對象就是開頭在構造函數中傳入的那個response參數,就是說,只要我們定義一個JSON格式的response對象傳給HTTPException的構造函數就能達到我們想要的效果了。

綜上所述,我們有兩種方法來實現我們的目的:

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

這兩種方法都可以通過定義一個繼承自HTTPException的子類來實現,以下我將分別實現這兩種方法。

自定義異常處理類

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

class APIException(HTTPException):
    code = 500  # http status code
    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__(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]

方法二:傳入一個JSON格式的response對象
直接上代碼,如下:

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

定義場景錯誤類

有了上面我們改寫好的APIException類,我們就可以自由的定義各種狀態碼的錯誤以及對應的錯誤信息,然後在合適的位置拋出即可,如下:

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


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

...

接下來做一個簡單的測試,在視圖函數中raise ParameterError, 然後使用curl命令請求接口:

@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"}

可以看到結果是完全符合預期的。這個例子充分體現了Flask的靈活性,這也是我喜愛Flask最重要的原因。同時,也說明HTTPException在設計時就已經考慮好了開發者對它的重構,使我們能方便實現自己的異常處理方式。

註冊全局錯誤處理函數

儘管可以在認爲可能出錯的所有地方,定義自己的錯誤類然後拋出,但是也不是所有的異常都是可以提前預知的。比如我們接收前端傳來的參數,參數類型或取值範圍不正確,這些我們可以預知並處理好,但是如果是邏輯處理中出現了問題,這些不是我們程序可以控制並處理的。所以光有自定義錯誤類還不夠,我們還需要在全局捕獲異常來判斷,利用AOP思想。

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

然後再在工廠函數中進行註冊:

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

關於flask的異常處理,以上就是我目前學習到的一些經驗技巧,如有錯誤歡迎指出,,後續會不斷更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章