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()方法考慮了兩種情況:
- 如果self.response對象不爲空,它就直接返回這個response對象作爲異常響應;
- 如果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的構造函數就能達到我們想要的效果了。
綜上所述,我們有兩種方法來實現我們的目的:
- 重寫get_headers()和get_body()方法;
- 傳入一個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的異常處理,以上就是我目前學習到的一些經驗技巧,如有錯誤歡迎指出,,後續會不斷更新。