异常提示
Pait
内部有很多参数校验逻辑,所以会出现多种错误情况,为了在使用的过程中方便地捕获和了解异常,Pait
拥有一个简单的异常机制。
Note
Pait
的异常都是继承于PaitBaseException
,在发生异常时可以通过:
isinstance(exc, PaitBaseException)
来判断异常是否属于
Pait
的异常。
此外, 由于Pait
是使用Pydantic
进行校验, 所以在运行时会因为校验不通过而抛出Pydantic
相关异常, 可以通过Error Handling了解如何使用Pydantic
异常
1.Pait异常介绍
1.1.TipException
在程序运行的时候,Pait
会对参数进行检查和校验,如果校验不通过,会抛出异常。
但是异常只会在Pait
中流转,不会被暴露出来从而使开发者无法了解异常是哪个路由函数抛出的,这样排查问题是十分困难的。
所以Pait
会通过TipException
对异常进行一个包装,在抛错信息里说明哪个路由函数抛错,抛错的位置在哪里,
如果用户使用Pycharm
等IDE工具,还可以通过点击路径跳转到对应的地方,一个异常示例如下:
Traceback (most recent call last):
File "/home/so1n/github/pait/.venv/lib/python3.7/site-packages/starlette/exceptions.py", line 71, in __call__
await self.app(scope, receive, sender)
File "/home/so1n/github/pait/.venv/lib/python3.7/site-packages/starlette/routing.py", line 583, in __call__
await route.handle(scope, receive, send)
File "/home/so1n/github/pait/.venv/lib/python3.7/site-packages/starlette/routing.py", line 243, in handle
await self.app(scope, receive, send)
File "/home/so1n/github/pait/.venv/lib/python3.7/site-packages/starlette/routing.py", line 54, in app
response = await func(request)
File "/home/so1n/github/pait/pait/core.py", line 232, in dispatch
return await first_plugin(*args, **kwargs)
File "/home/so1n/github/pait/pait/param_handle.py", line 448, in __call__
async with self:
File "/home/so1n/github/pait/pait/param_handle.py", line 456, in __aenter__
raise e from gen_tip_exc(self.call_next, e)
File "/home/so1n/github/pait/pait/param_handle.py", line 453, in __aenter__
await self._gen_param()
File "/home/so1n/github/pait/pait/param_handle.py", line 439, in _gen_param
self.args, self.kwargs = await self.param_handle(func_sig, func_sig.param_list)
File "/home/so1n/github/pait/pait/param_handle.py", line 396, in param_handle
await asyncio.gather(*[_param_handle(parameter) for parameter in param_list])
File "/home/so1n/github/pait/pait/param_handle.py", line 393, in _param_handle
raise gen_tip_exc(_object, closer_e, parameter)
pait.exceptions.TipException: Can not found content__type value for <function raise_tip_route at 0x7f512ccdebf8> Customer Traceback:
File "/home/so1n/github/pait/example/param_verify/starlette_example.py", line 88, in raise_tip_route.
可以看到异常是通过gen_tip_exc
抛出来的,而抛出来的异常信息则包含路由函数所在位置。
不过使用了TipException
还有一个弊端,它会导致所有异常都是TipException
需要通过TipException.exc
获取到原本的异常。
1.2.参数异常
目前Pait
有3种参数异常,如下:
异常 |
出现位置 |
说明 |
NotFoundFieldException |
预检查阶段 |
该异常表示匹配不到对应的Field ,正常使用时,不会遇到该异常。 |
NotFoundValueException |
路由函数命中阶段 |
该异常表示无法从请求数据中找到对应的值,这是一个常见的异常 |
FieldValueTypeException |
预检查阶段 |
该异常表示Pait 发现Field 里的default ,example 等填写的值不合法,使用者需要根据提示进行改正。 |
这三种异常都是继承于PaitBaseParamException
,它的源码如下:
class PaitBaseParamException(PaitBaseException):
def __init__(self, param: str, msg: str):
super().__init__(msg)
self.param: str = param
self.msg: str = msg
从代码可以看出PaitBaseParamException
在抛异常时只会抛出错误信息,但是在需要根据异常返回一些指定响应时,可以通过param
知道是哪个参数出错。
2.如何使用异常
2.1.异常的使用
在CRUD业务中,路由函数抛出的异常都要被捕获,然后返回一个协定好的错误信息供前端使用,下面是一个异常捕获的示例代码:
docs_source_code/introduction/exception/flask_with_exception_demo.py |
---|
| from typing import List
from flask import Flask, Response, jsonify
from pydantic import ValidationError
from pait import exceptions, field
from pait.app.flask import pait
def api_exception(exc: Exception) -> Response:
if isinstance(exc, exceptions.TipException):
exc = exc.exc
if isinstance(exc, exceptions.PaitBaseParamException):
return jsonify({"code": -1, "msg": f"error param:{exc.param}, {exc.msg}"})
elif isinstance(exc, ValidationError):
error_param_list: List = []
for i in exc.errors():
error_param_list.extend(i["loc"])
return jsonify({"code": -1, "msg": f"check error param: {error_param_list}"})
elif isinstance(exc, exceptions.PaitBaseException):
return jsonify({"code": -1, "msg": str(exc)})
return jsonify({"code": -1, "msg": str(exc)})
@pait()
def demo(demo_value: int = field.Query.i()) -> Response:
return jsonify({"code": 0, "msg": "", "data": demo_value})
app = Flask("demo")
app.add_url_rule("/api/demo", view_func=demo, methods=["GET"])
app.errorhandler(Exception)(api_exception)
if __name__ == "__main__":
app.run(port=8000)
|
docs_source_code/introduction/exception/starlette_with_exception_demo.py |
---|
| from typing import List
from pydantic import ValidationError
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from pait import exceptions, field
from pait.app.starlette import pait
async def api_exception(request: Request, exc: Exception) -> JSONResponse:
if isinstance(exc, exceptions.TipException):
exc = exc.exc
if isinstance(exc, exceptions.PaitBaseParamException):
return JSONResponse({"code": -1, "msg": f"error param:{exc.param}, {exc.msg}"})
elif isinstance(exc, ValidationError):
error_param_list: List = []
for i in exc.errors():
error_param_list.extend(i["loc"])
return JSONResponse({"code": -1, "msg": f"check error param: {error_param_list}"})
elif isinstance(exc, exceptions.PaitBaseException):
return JSONResponse({"code": -1, "msg": str(exc)})
return JSONResponse({"code": -1, "msg": str(exc)})
@pait()
async def demo(demo_value: int = field.Query.i()) -> JSONResponse:
return JSONResponse({"code": 0, "msg": "", "data": demo_value})
app = Starlette(routes=[Route("/api/demo", demo, methods=["GET"])])
app.add_exception_handler(Exception, api_exception)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app)
|
docs_source_code/introduction/exception/sanic_with_exception_demo.py |
---|
| from typing import List
from pydantic import ValidationError
from sanic import HTTPResponse, Request, Sanic, json
from pait import exceptions, field
from pait.app.sanic import pait
async def api_exception(request: Request, exc: Exception) -> HTTPResponse:
if isinstance(exc, exceptions.TipException):
exc = exc.exc
if isinstance(exc, exceptions.PaitBaseParamException):
return json({"code": -1, "msg": f"error param:{exc.param}, {exc.msg}"})
elif isinstance(exc, ValidationError):
error_param_list: List = []
for i in exc.errors():
error_param_list.extend(i["loc"])
return json({"code": -1, "msg": f"check error param: {error_param_list}"})
elif isinstance(exc, exceptions.PaitBaseException):
return json({"code": -1, "msg": str(exc)})
return json({"code": -1, "msg": str(exc)})
@pait()
async def demo(demo_value: int = field.Query.i()) -> HTTPResponse:
return json({"code": 0, "msg": "", "data": demo_value})
app = Sanic("demo")
app.add_route(demo, "/api/demo", methods={"GET"})
app.exception(Exception)(api_exception)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app)
|
docs_source_code/introduction/exception/tornado_with_exception_demo.py |
---|
| from typing import List
from pydantic import ValidationError
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler
from pait import exceptions, field
from pait.app.tornado import pait
from pait.openapi.doc_route import AddDocRoute
class _Handler(RequestHandler):
def _handle_request_exception(self, exc: BaseException) -> None:
if isinstance(exc, exceptions.TipException):
exc = exc.exc
if isinstance(exc, exceptions.PaitBaseParamException):
self.write({"code": -1, "msg": f"error param:{exc.param}, {exc.msg}"})
elif isinstance(exc, ValidationError):
error_param_list: List = []
for i in exc.errors():
error_param_list.extend(i["loc"])
self.write({"code": -1, "msg": f"check error param: {error_param_list}"})
elif isinstance(exc, exceptions.PaitBaseException):
self.write({"code": -1, "msg": str(exc)})
self.finish()
class DemoHandler(_Handler):
@pait()
async def get(self, demo_value: int = field.Query.i()) -> None:
self.write({"code": 0, "msg": "", "data": demo_value})
app: Application = Application([(r"/api/demo", DemoHandler)])
AddDocRoute(app)
if __name__ == "__main__":
app.listen(8000)
IOLoop.instance().start()
|
代码中api_exception
函数的异常处理是按照严格的顺序排列的,一般情况下建议以这种顺序处理异常。
api_exception
函数的第一段高亮是提取TipException
的原本异常,后面的所有异常处理都是针对于原本的异常,所以优先级最高。
第二段高亮是处理所有Pait
的参数异常,它会提取参数信息和错误信息,告知用户哪个参数发生错误。
第三段高亮代码处理的是Pydantic
的校验异常,这里会解析异常,并返回校验失败的参数信息。
第四段代码处理的是Pait
的所有异常,通常很少出现,直接返回异常信息,最后是处理其他情况的异常,这里可能是业务系统定义的异常。
最后一段高亮代码是把自定义的api_exception
函数挂载到框架的异常处理回调中。
Tornado
的异常处理是在RequestHandler
实现的。
在运行代码并调用curl
命令后可以发现:
- 缺少参数时,会返回找不到参数的错误信息
➜ ~ curl "http://127.0.0.1:8000/api/demo"
{"code":-1,"msg":"error param:demo_value, Can not found demo_value value"}
- 参数校验出错时,会返回校验出错的参数名
➜ ~ curl "http://127.0.0.1:8000/api/demo?demo_value=a"
{"code":-1,"msg":"check error param: ['demo_value']"}
- 参数正常时返回正常的数据
➜ ~ curl "http://127.0.0.1:8000/api/demo?demo_value=3"
{"code":0,"msg":"","data":3}
交互协议说明
示例代码的响应使用了常见的前后端交互协议:
{
"code": 0, # 为0时代表响应正常,不为0则为异常
"msg": "", # 异常时为错误信息,正常时为空
"data": {} # 正常响应时的数据
}
其中
code
为0时代表响应正常,不为0则为异常且
msg
包括了一个错误信息供前端展示,而
data
是正常响应时的结构体。
2.2.自定义Tip异常
Tip异常是默认启用的,如果在使用的过程中觉得错误提示会消耗性能或者觉得错误提示没啥作用,可以把ParamHandler
的tip_exception_class
属性定义为None
来关闭异常提示,代码如下:
docs_source_code/introduction/exception/flask_with_not_tip_exception_demo.py |
---|
| from typing import List
from flask import Flask, Response, jsonify
from pydantic import ValidationError
from pait import exceptions, field
from pait.app.flask import pait
from pait.param_handle import ParamHandler
class NotTipParamHandler(ParamHandler):
tip_exception_class = None
def api_exception(exc: Exception) -> Response:
if isinstance(exc, exceptions.PaitBaseParamException):
return jsonify({"code": -1, "msg": f"error param:{exc.param}, {exc.msg}"})
elif isinstance(exc, ValidationError):
error_param_list: List = []
for i in exc.errors():
error_param_list.extend(i["loc"])
return jsonify({"code": -1, "msg": f"check error param: {error_param_list}"})
elif isinstance(exc, exceptions.PaitBaseException):
return jsonify({"code": -1, "msg": str(exc)})
return jsonify({"code": -1, "msg": str(exc)})
@pait(param_handler_plugin=NotTipParamHandler)
def demo(demo_value: int = field.Query.i()) -> Response:
return jsonify({"code": 0, "msg": "", "data": demo_value})
app = Flask("demo")
app.add_url_rule("/api/demo", view_func=demo, methods=["GET"])
app.errorhandler(Exception)(api_exception)
if __name__ == "__main__":
app.run(port=8000)
|
docs_source_code/introduction/exception/starlette_with_not_tip_exception_demo.py |
---|
| from typing import List
from pydantic import ValidationError
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from pait import exceptions, field
from pait.app.starlette import pait
from pait.param_handle import AsyncParamHandler
class NotTipAsyncParamHandler(AsyncParamHandler):
tip_exception_class = None
async def api_exception(request: Request, exc: Exception) -> JSONResponse:
if isinstance(exc, exceptions.PaitBaseParamException):
return JSONResponse({"code": -1, "msg": f"error param:{exc.param}, {exc.msg}"})
elif isinstance(exc, ValidationError):
error_param_list: List = []
for i in exc.errors():
error_param_list.extend(i["loc"])
return JSONResponse({"code": -1, "msg": f"check error param: {error_param_list}"})
elif isinstance(exc, exceptions.PaitBaseException):
return JSONResponse({"code": -1, "msg": str(exc)})
return JSONResponse({"code": -1, "msg": str(exc)})
@pait(param_handler_plugin=NotTipAsyncParamHandler)
async def demo(demo_value: int = field.Query.i()) -> JSONResponse:
return JSONResponse({"code": 0, "msg": "", "data": demo_value})
app = Starlette(routes=[Route("/api/demo", demo, methods=["GET"])])
app.add_exception_handler(Exception, api_exception)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app)
|
docs_source_code/introduction/exception/sanic_with_not_tip_exception_demo.py |
---|
| from typing import List
from pydantic import ValidationError
from sanic import HTTPResponse, Request, Sanic, json
from pait import exceptions, field
from pait.app.sanic import pait
from pait.param_handle import AsyncParamHandler
class NotTipAsyncParamHandler(AsyncParamHandler):
tip_exception_class = None
async def api_exception(request: Request, exc: Exception) -> HTTPResponse:
if isinstance(exc, exceptions.PaitBaseParamException):
return json({"code": -1, "msg": f"error param:{exc.param}, {exc.msg}"})
elif isinstance(exc, ValidationError):
error_param_list: List = []
for i in exc.errors():
error_param_list.extend(i["loc"])
return json({"code": -1, "msg": f"check error param: {error_param_list}"})
elif isinstance(exc, exceptions.PaitBaseException):
return json({"code": -1, "msg": str(exc)})
return json({"code": -1, "msg": str(exc)})
@pait(param_handler_plugin=NotTipAsyncParamHandler)
async def demo(demo_value: int = field.Query.i()) -> HTTPResponse:
return json({"code": 0, "msg": "", "data": demo_value})
app = Sanic("demo")
app.add_route(demo, "/api/demo", methods={"GET"})
app.exception(Exception)(api_exception)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app)
|
docs_source_code/introduction/exception/tornado_with_not_tip_exception_demo.py |
---|
| from typing import List
from pydantic import ValidationError
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler
from pait import exceptions, field
from pait.app.tornado import pait
from pait.openapi.doc_route import AddDocRoute
from pait.param_handle import AsyncParamHandler
class NotTipAsyncParamHandler(AsyncParamHandler):
tip_exception_class = None
class _Handler(RequestHandler):
def _handle_request_exception(self, exc: BaseException) -> None:
if isinstance(exc, exceptions.PaitBaseParamException):
self.write({"code": -1, "msg": f"error param:{exc.param}, {exc.msg}"})
elif isinstance(exc, ValidationError):
error_param_list: List = []
for i in exc.errors():
error_param_list.extend(i["loc"])
self.write({"code": -1, "msg": f"check error param: {error_param_list}"})
elif isinstance(exc, exceptions.PaitBaseException):
self.write({"code": -1, "msg": str(exc)})
self.finish()
class DemoHandler(_Handler):
@pait(param_handler_plugin=NotTipAsyncParamHandler)
async def get(self, demo_value: int = field.Query.i()) -> None:
self.write({"code": 0, "msg": "", "data": demo_value})
app: Application = Application([(r"/api/demo", DemoHandler)])
AddDocRoute(app)
if __name__ == "__main__":
app.listen(8000)
IOLoop.instance().start()
|
示例代码总共有三处修改:
- 第一段高亮代码中的
NotTipParamHandler
继承于ParamHandler
(或者AsyncParamHandler
),它通过设置tip_exception_class
属性为空来关闭异常提示。
- 第二段高亮代码则是把
TipException
的提取逻辑从api_exception
函数中移除,因为现在不需要了。
- 第三段高亮代码通过
Pait
的param_handler_plugin
属性定义当前路由函数使用的ParamHandler
为NotTipParamHandler
。
在运行代码并通过curl
调用可以发现程序正常运行,如下:
- 缺少参数时,会返回找不到参数的错误信息
➜ ~ curl "http://127.0.0.1:8000/api/demo"
{"code":-1,"msg":"error param:demo_value, Can not found demo_value value"}
- 参数校验出错时,会返回校验出错的参数名
➜ ~ curl "http://127.0.0.1:8000/api/demo?demo_value=a"
{"code":-1,"msg":"check error param: ['demo_value']"}
- 参数正常时返回正常的数据
➜ ~ curl "http://127.0.0.1:8000/api/demo?demo_value=3"
{"code":0,"msg":"","data":3}