Python接口开发的异常处理

我们在开发restful接口的时候,一定会经常遇到异常的情况,包括可预知的异常和不可预知的异常,我们要怎么处理这些异常,才能既优雅的写好代码,又能让程序接口健壮性更好呢?

首先分析下,遇到错误的时候,我们有两种方法,可以把信息返回给客户端:

  1. 遇到错误,直接返回一个jsonify的json信息给前端
  2. 抛出一个错误,然后我们在定义错误的handler专门捕捉错误,然后把错误里面的信息,包括错误码,错误信息,都同意填到我们需要返回的json里面。

我们来说说第二种情况,定义各种错误异常,然后抛出,最后在error_hanlder里面捕捉异常统一返回,这样做的目的与几个好处:

  • 直接抛出错误,然后在捕捉器统一返回,捕捉器这里可以统一做一下日志记录
  • 捕捉器根据不同的异常,做不同的处理
  • 捕捉器能获取到异常这个对象,那么异常是我们抛出来的,所以异常的定义可以把很多信息封装进去,比如错误码,错误信息。

看下面error_handler的处理,也就是统一的异常捕捉器

def register_error_handler(app):
    @app.errorhandler(errors.BaseError)
    def base_error(err):
        g.err = err 
        g.resp_data = err.reason.message
        return jsonify(err.to_json()), err.status_code

    @app.errorhandler(404)
    def page_not_found(err):
        g.err = errors.ResourceNotFoundError(
            errors.REQ_URL_INVALID_ERROR, 'invalid url')
        return jsonify({'message': str(err)}), 404 

    @app.errorhandler(405)
    def method_not_allowed(err):
        g.err = errors.BadRequestError(
            errors.METHOD_NOT_ALLOWED_ERROR, 'invalid http method')
        return jsonify({'message': str(err)}), 405 

    @app.errorhandler(Exception)
    def unknown_error(err):
        g.err = errors.InternalServerError(
            errors.SERVER_UNEXPECTED_ERROR,
            'unknown error: %s' % err,
            exc_info=sys.exc_info())
        g.resp_data = g.err.reason.message
        return jsonify(g.err.to_json()), g.err.status_code

上面的代码,可以看出,第一个捕捉器是专门捕捉BaseError,也就是我们自定义的异常,可以捕捉到不止BaseError,还能BaseError的所有子类。所有继承与它的子异常都可以被捕捉到。
第二和第三个就是捕捉http状态码是404 405的异常,然后返回对应的错误消息。

异常和原因类

既然我们抛出的异常,被捕捉的时候,是可以获取到,并且读到它的属性的,那么我们可以在这个时候,把我们自定义的错误码->错误信息等信息传递过去给异常。捕捉的时候就可以读出来。

那么我们可以顶一个错误原因类,ErrorReason,实例化的时候,把错误码,错误信息,日志的等级也封装进去,成为一个对象,然后把这个对象,传给异常,那么读取异常的时候,就可以Error.reason.msg 这种封装形式来读取了。这样就很OOP了。

# 错误原因类
class ErrorReason(object):
    ''' 
    异常原因代码
    '''
    def __init__(self, code, message, log_level=logging.ERROR):
        self.code = code
        self.message = message
        self.log_level = log_level

# 各种错误理由的实例,生成ErrorReason对象,这时这些属性已经封装到对象里面
REQ_PARAMS_ERROR = ErrorReason(4000, u'用户请求参数错误', log_level=logging.WARNING)
ILLEGAL_CHAR_ERROR = ErrorReason(4000, u'输入包含非法字符,请返回修改', log_level=logging.WARNING)
APP_NOT_EXIST_ERROR = ErrorReason(4000, u'应用不存在', log_level=logging.WARNING)
CATEGORY_NOT_EXIST_ERROR = ErrorReason(4000, u'分类不存在', log_level=logging.WARNING)
DEVELOPER_NOT_EXIST_ERROR = ErrorReason(4000, u'应用开发商不存在', log_level=logging.WARNING)
CHANNEL_NOT_EXIST_ERROR = ErrorReason(4000, u'应用渠道不存在', log_level=logging.WARNING)
PACKAGE_NOT_EXIST_ERROR = ErrorReason(4000, u'应用包不存在', log_level=logging.WARNING)

这样实例化不同的错误原因对象,每个对象都封装了自己的信息。

下面看看错误类,我们自己子类化一个BaseError,它是其他所有的异常的父类。重写了__init__()方法,把reason, message, status_code都封装进去,好让被捕捉的时候,可以知道获取错误原因e.reason, e.reason.code,..错误消息e.message, 和http状态码,e.status_code

# 错误类,继承Exception
class BaseError(Exception):

    def __init__(self, reason, message, status_code=500, exc_info=None):
        if isinstance(message, unicode):
            message = message.encode('utf-8')
        self.reason = reason
        self.message = message
        self.status_code = status_code
        self.exc_info = exc_info

    def to_json(self):
        return {
            "status": self.reason.code,
            "reason": self.reason.message,
            "message": self.message,
        }  

下面就是继承各种子类

class BadRequestError(BaseError):

    def __init__(self, reason, message, exc_info=None):
        super(BadRequestError, self).__init__(reason, message, 400, exc_info)


class PermissionError(BaseError):

    def __init__(self, reason, message, exc_info=None):
        super(PermissionError, self).__init__(reason, message, 403, exc_info)


class ResourceNotFoundError(BaseError):

    def __init__(self, reason, message, exc_info=None):
        super(ResourceNotFoundError, self).__init__(
            reason, message, 404, exc_info)

可以看到,BadRequestError、PermissionError继承的时候,继承了__init__(), 还是调用了父类的__init__()方法,但是就是固定的把403, 400传进去。很多这种继承都是这样,就是把某一个参数固定了,它就变成了一个新的类,但是这样的类一眼就能看出是干嘛的,因为的它的名字很有含义,然后有把代表性的值设成了固定值。

所以我们在调用的时候,遇到有什么不妥的地方,直接抛出错误。

if perm not in consts.PermKey.SCHEMAS:
    raise errors.BadRequestError(
         errors.BAD_REQ_ERROR, u'Unknown permission')

if not Perm.cancan(perm, g.admin.id) and g.admin.username not in app_config['ADMIN_USER_LIST']:
    raise errors.PermissionError(
          errors.BAD_REQ_ERROR, u'No permission for %s' % perm)

这样就是可以把异常抛出,同时把定义好的错误理由也封装进去。
我们有了异常捕捉器,基本上整个网站是不会报错500,错误的页面的,而是相对友好的返回一段json。

def handler_api_error():
    @api_handler.errorhandler(Exception)
    def _handler_app_error(error):
        if isinstance(error, errors.BaseError):
            reason = error.reason
        else:
            reason = errors.SERVER_UNEXPECTED_ERROR

        return Adaptor.result(
                code=reason.code, msg=reason.message,
                detail=error.message, log_level=reason.log_level,
                exc_info=reason.log_level >= logging.ERROR)

class Adator(object)
    @classmethod
    def result(cls, code=0, msg=u'操作成功', detail=None,
               log_level=logging.DEBUG, exc_info=False, **kargs):
        log_data = {u'code': code, u'msg': msg}
        if kargs:
            log_data.update(cls._mask_resp(kargs))

        cls._log_api(log_level, u'json', log_data,
                     detail=detail, exc_info=exc_info)
        return jsonify(code=code, msg=msg, **kargs)

最后看一下这个error_handler,捕捉所有异常Exception, 然后进去后,判断是否我们自己定义的BaseError类型,如果是,则读出它的reason属性,如果不是,则把错误的reason赋值成一个我们的子类化的一个错误原因。 最后通过调用result()返回json化这段错误。result()的主要工作就是log下请求内容,请求的方法,返回的接口内容等信息。
所以,无论使我们自己手动调用Adator.result() , 还是通过抛出异常,然后error_handler捕捉,最后还是调用Adator.result(),这样就形成了一个最终一致性,都是调用Adator.result(),那种情况都可以正常返回json,并且都能记录日志!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 早期阅读对孩子以后喜爱读书非常有好处。只要时间允许,就频繁地为你的孩子朗读。每天至少安排一段固定的读故事时间,尝试...
    ALICE_微笑阅读 211评论 0 4
  • 处理流时记号要不要关闭流。。。。。。。。 将print转换为print到文件中的要点:1、新建一个输出流2、将输出...
    exmexm阅读 175评论 0 0
  • -START-9月19日,晴,有些热,不过这温度也就在“挣扎”几天吧,到了国庆节就不行了。 说起来也大学毕业有几年...
    余温夏暖阅读 487评论 0 2
  • 每一座城都有着它本身独特的文化与历史的积淀,它承载着的是几代人的回忆与情怀,如此又岂是简单的建筑复制所能汲取的?...
    小人豆子阅读 176评论 0 0