跳转至

异常提示

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里的defaultexample等填写的值不合法,使用者需要根据提示进行改正。

这三种异常都是继承于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异常是默认启用的,如果在使用的过程中觉得错误提示会消耗性能或者觉得错误提示没啥作用,可以把ParamHandlertip_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函数中移除,因为现在不需要了。
  • 第三段高亮代码通过Paitparam_handler_plugin属性定义当前路由函数使用的ParamHandlerNotTipParamHandler

在运行代码并通过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}