[python] pprika:基于werkzeug编写的web框架(6) ——restful的错误处理

序言

restful作为本框架pprika的特色,其文件大小比之前介绍的其他模块(不包括核心部分app.py)加起来都大...而它的部分功能,指自动将dict、list类型返回值处理为json响应,也由make_response实现了。那么接下来准备谈谈restful中较为亮眼的部分——错误处理,对,又是错误处理,除此之外还有类似flask-restful一般用于组织代码的Resource类、便于验证参数的RequestParser类等。

接下来先从示例开始。

 

异常的处理

from pprika import Api

v1 = Api('v1')


@v1.route('/cats')
def cat():
    raise TypeError('这里没有猫')

请求该url将返回http500错误: {"message":"TypeError('这里没有猫')"},好像跟flask-restful相比没有优势?

flask-restful可以在初始化Api实例时传入error字典处理异常,如 v1 = Api(v1, errors=errors) ,但这个错误字典编写复杂还不易修改,而且键message是写死的,也就是说响应必然带有一个"message": "xx"

若通过抛出HTTPException及其子类的方式一定程度可以定制响应,但响应格式还是无法跟其他异常类统一

还有一点,flask-restful的restful功能与app、blueprint并不对等,相当于依附于后者上。此时若使用app或blueprint的错误注册,如 @main.error_handler(500) ,将无法成功应用该handler,原因就是flask_restful.Api有默认的json形式错误处理,其优先于flask.handle_user_exception,导致error_handler基本失效。

 

而在pprika中这些问题都不存在,错误可以被轻易且形式统一地定制,如上例可变为 {"code":500,"msg":"TypeError('这里没有猫')"} 。而且pprika.Api是Blueprint子类,拥有blueprint的注册方式,通过 @v1.error_handler 注册的处理器会优先于默认错误处理器调用,还可以在Api实例化时传入自定义异常类改变默认处理器的行为,如 v1 = Api('v1', exception_cls=CustomException) 

那么pprika.Api是如何做到这些的?实际上也很简单↓

 

Api

class Api(Blueprint):
    exception_cls = ApiException

    def __init__(self, name, url_prefix=None, exception_cls=None):
        super().__init__(name, url_prefix)
        if exception_cls is not None:
            self.exception_cls = exception_cls
        self._deferred_funcs.append(lambda a: self._init_app(a))

 ApiException是用于错误处理的基础类,可通过更改exception_cls替换。

这里做三件事:初始化父类Blueprint的属性、尝试替换exception_cls、延迟调用 _init_app。与蓝图一样,_deferred_funcs内的函数将在app.register_blueprint时被调用。

 

_init_app

 from functools import partial
 
    def _init_app(self, app):
        if not app.api_set:
            app.handle_exception = partial(self._error_router, app.handle_exception)
            # 若自身未设置对应错误处理器,则错误由handle_user_exception中再抛出
        app.api_set.add(self.name)

 

该函数负责Api的初始化,但它所做的其实只是把app.handle_exception换成了Api内部的self._error_router,(相当于在错误处理器外多加了一层用于处理器选择),这样之后进行错误处理时就有机会根据错误来源,对错误运用不同的处理方式。

而app.api_set即是PPrika的一个属性,是存储了每个注册到app上的Api名称的集合,通过对该集合判断来确保多个Api注册到app上时只将handle_exception 替换了一次。

ps:在flask-restful中将app的两层处理器(handle_user_exception、handle_exception)都进行了partial操作,也是因为它同时改变了内层处理器行为,导致发生于Api内部的错误总会被Api的默认处理器处理,没有机会使用该Api所附着的app/blueprint设置的处理器。

 

_error_router

    def _error_router(self, original_handler, e):
        if request.blueprint == self.name:
            return self.handle_error(e)
            # api中的错误不使用original_handler
        return original_handler(e)

这个函数将根据错误的是否发生在Api内部来选择使用Api上的错误处理handle_error,还是原来的处理器(app上的handle_exception ),不同于flask-restful,这是二选一的问题,因此Api内部的错误总能按json方式响应。

ps:在flask-restful中该函数行为是若错误发生于Api内部,则先用Api内部的处理器,不行再使用原有的

 

handle_error

 1     def handle_error(self, e):
 2         if isinstance(e, self.exception_cls):
 3             pass
 4         elif isinstance(e, HTTPException):
 5             e = self.exception_cls(e.code, e.description)
 6         elif isinstance(e, ApiException):
 7             e = self.exception_cls(e.status, e.message)
 8         else:
 9             print_exception(*exc_info())
10             e = self.exception_cls(500, repr(e))
11         return e.get_response()

看着一大坨,但目的只有一个:根据错误实例e的不同type,将不同的e都统一实例化为 self.exception_cls 类对象,并使用该对象上的方法get_response形成响应对象。

其中第8行开始的else语句意味着e为一般的异常实例,统一按照http码500响应。

ps:之前在请求上下文ctx那里也提到,类似于404/405的路由错误不会被任何蓝图(当然保存Api)的处理器捕捉,除非是在视图函数中人为抛出的。若对统一的json响应有严格要求可以继承Api类,重写_error_router函数,捕捉所有的404/405。

 

ApiException

class ApiException(Exception):
    status = None
    message = None

    def __init__(self, status=None, message=None):
        self.status = status or self.status
        self.message = message or self.message

    def get_response(self):
        body = {'message': self.message}
        rv = body, self.status
        return make_response(rv)

这个类就是Api.handle_error中用到的self.exception_cls,如果exception_cls属性未被设置的话。它根据响应码status与响应内容message调用make_response构造响应对象。

该类实现了__str__与__repr__方法,实现方式没有特殊要求,有就行,在此略过

通过继承该类(或完全重新写一个)可自定义统一的错误响应格式 ↓

from pprika import ApiException

class CustomException(ApiException):
    pass


v1 = Api('v1', exception_cls=CustomException)

具体示例参见源码中 pprika/app/v1/ 这一部分。

通过这个方法几乎可随意更改格式,但要注意self.exception_cls需要用于 self.handle_error 去转化其他的异常,而这种实例化总是会提供两个参数,因此定制的exception_cls若有新增属性并且用于get_response中,那么要在__init__中初始化这个属性,同时要记住__init__要考虑只接收两个参数的情况。

 

结语

这样restful的错误处理就完成了,接下来谈谈如何像flask-restful那般使用一个Resource类组织一个url下的所有method,以及简单参数验证RequestParser。

[python] pprika:基于werkzeug编写的web框架(7) ——restful的结构化与参数解析

 

posted @ 2020-05-31 21:41  NoNoe  阅读(176)  评论(0编辑  收藏  举报