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()方法考慮了兩種情況:
- 如果self.response對(duì)象不為空他去,它就直接返回這個(gè)response對(duì)象作為異常響應(yīng)津函;
- 如果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)我們的目的:
- 重寫get_headers()和get_body()方法;
- 傳入一個(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ì)不斷更新抵怎。