目录

    笔者目前使用 Django 从事 SaaS 开发,同时开发和维护多个 SaaS 应用。在很多 SaaS 应用中都约定了错误码,有的用于处理登录态,有的用于标记业务逻辑状态。对于这种项目共性很强的特征,花时间学习和研究是非常有必要的。本篇主要讨论了错误码的用途、如何设计错误码、使用 Django 中间件如何实现异常的处理错误码的返回。

    1. 错误码的用途

    错误码是与错误信息关联的一组数字或字母,用于约定错误状态。

    在 Web 应用中,一次接口的访问,涉及反向代理的转发、业务逻辑的处理、数据库的访问、模板的渲染、中间件的处理等环节,难免会出现各种各样的错误。同时,系统越复杂,访问的链路越长,模块越多,出错的可能性越高。

    既然错误无法避免,那么就需要反馈错误信息。返回错误信息,一方面是为了在应用系统开发阶段,方便调试,添加相应的逻辑处理,提示用户;另一方面是应用系统运行时,可能会有潜在的异常风险,错误码能辅助定位和修复问题。

    HTTP 状态码是最常见的,通过提前协商处理服务状态的约定编码。HTTP 状态码通常由三个数字组成,第一个数字定义了响应的类别,且只有五种可能值。

    状态码范围含义
    1xx100-101指示信息–表示请求已接收,继续处理
    2xx200-206成功–表示请求已被成功接收、理解、接受
    3xx300-305重定向–信息不完整需要进一步补充
    4xx400-415客户端错误–请求有语法错误或请求无法实现
    5xx500-505服务器端错误–服务器未能实现合法的请求

    值得注意的是,HTTP 状态码可以通过小数的方式扩展,更加详细的描述服务器状态,比如,403 表示禁止访问,403.1 表示禁止可执行访问,403.2 表示禁止读访问。

    通过 HTTP 状态码,客户端能够有效地获取服务器的响应状态,更好地处理异常情况、提示用户信息。错误码和 HTTP 状态码有异曲同工之处,不同的是错误码约定的是业务逻辑,而 HTTP 状态码约定的是服务器的响应状态。

    在 Web 应用的通信过程中,如果 HTTP 状态码不足以表示服务器的响应状态,可以通过错误码来补充,比如,服务器返回 {'code': 500101, 'message': u'连接数据库错误'}。HTTP 状态码是一种已经达成共识的约定编码,而错误码需要建立新的约定。在业务逻辑中,也可以通过 HTTP 状态码表示业务错误状态,比如使用 412 表示未满足前提条件。错误码和 HTTP 状态码有交集,但不能相互替代。

    2. 如何设计错误码

    2.1 一些公共平台的错误码

    错误码主要分为两类
    (1) 小于100 错误码,表示用户请求不符合基本校验,比如,字段校验、权限、频率等。
    (2) 子错误码,以 “isp.” 开头,表示服务端异常,比如 “isp.remote-service-error”、”isp.remote-service-timeout”等。不同的服务,使用不同的头部。
    还有一些特定约定的错误码,比如 801、802等。

    错误码说明:
    (1) ret = 0,正确返回
    (2) ret > 0,调用 OpenAP I时发生错误,需要开发者进行相应的处理。
    (3) -50 <= ret <= -1, 接口调用不能通过接口代理机校验,需要开发者进行相应的处理。
    (4) ret <-50,系统内部错误

    另外,腾讯开放平台提供的各种语言的 SDK 错误码含义相同。使用数字表示,比如 1801、1802、1803等。

    以三位和四位的数字为主,下面是部分错误码:

    错误代码错误类型说明
    0成功调用成功
    401<HTTP请求参数不符合要求HTTP请求参数不符合要求
    503调用额度已超出限制调用额度已超出限制
    504服务故障服务故障
    4000请求参数非法缺少必要参数,或者参数值格式不正确,具体
    6000服务器内部错误服务器内部出现错误,请稍后重试或者联系客服人员帮忙解决。

    腾讯开放平台支付的错误码,以短横线连接三组数字表示。从错误码字母分析,是以短横线分隔不同模块,或者表示不同处理阶段,但是在官方文档上并没有明确说明。下面是部分错误码:

    错误码:1003-498493-106
    错误码:1003-498692-106
    错误码:1025-1025-0
    错误码:1043-10053-0
    错误码:1058-498198-40000
    错误码:1058-500952-40000
    错误码:1058-500954-40000

    错误码格式

    JSON
    {
        "request" : "/statuses/home_timeline.json",
        "error_code" : "20502",
        "error" : "Need you follow uid."
    }
    

    错误代码说明,以 20502 为例

    20502
    服务级错误(1为系统级错误)服务模块代码具体错误代码

    部分错误码:

    错误代码错误信息详细描述
    10014服务模Insufficient app permissions应用的接口访问权限受限
    20603List does not exists列表不存在
    20701Repeated tag text不能提交相同的收藏标签

    百度开发者中心的错误码,采用自增的方式编码。

    错误代码错误信息详细描述
    0成功Success
    1未知错误Unknown error
    2服务暂不可用Service temporarily unavailable
    100请求参数无效Invalid parameter
    101api key无效Invalid API key
    102session key无效Session key invalid or no longer valid
    103call_id参数无效Invalid/Used call id parameter

    微信开发平台采用的是五位错误码。

    错误代码错误信息详细描述
    40001invalid credential不合法的调用凭证
    40008invalid message type不合法的message_type
    40016invalid button size不合法的菜单按钮个数

    在微信支付相关的接口中,采用的是英文大写字母加下划线的方式编码。

    错误代码错误信息详细描述
    NOAUTH商户无此接口权限商户未开通此接口权限
    ORDERPAID商户订单已支付,无需重复操作商户订单已支付,无需更多操作
    SYSTEMERROR系统错误系统超时

    2.2 好的错误码有哪些特征

    • 长度足够短
      在满足使用需求、考虑扩展的情况下,短的错误码更方便维护和更新。腾讯开放平台的错误码,就显得特别冗长,即使遇到过一次错误,第二次出现时,也很难让人想起来。
    • 包含更多信息
      新浪开放平台的错误码通过首位区分系统、服务级的错误。后面紧跟着模块代码和具体错误代码,非常容易定位错误。包含更多信息,意味着更长的错误码,这与长度足够短的建议相冲突。怎样选择合适的长度,需要考虑系统的复杂性。如果系统很复杂、需要表示很多状态,那么当然是优先满足系统需要,使用长的错误码,包含更多的错误信息。
    • 字面能够望文生义
      微信支付平台的错误码,就特别容易理解其含义。通过几个简单的动作和关键字组合,比如,NO、LACK、DATA、PARAMS,不需要错误码对照表,就能八九不离十的猜到错误含义。当然,还有些比较长的错误码,OUT_TRADE_NO_USED, 编码和理解会比较费力。
    • 充分利用达成共识的编码
      返回为0 ,表示请求正常,返回为 <0 ,表示异常,不需要文字的说明,使用达成共识的编码能显著降低沟通的成本。需要注意的是还有一个共识,特别是 Web 开发者,HTTP 状态码是最重要的编码共识。腾讯开放平台和微信开发平台都采用了大量 4XXX 表示客户端错误,而 5XXX 表示服务内部移除。不需要查看错误码对照表,开发者就能基本定位哪里发生了错误,再利用错误码对照表就能具体到程序逻辑的错误。

    2.3 错误码设计

    2.3.1 根据模块划分编码

    第1位第2-3位第4-5位
    20502
    服务级错误(1为系统级错误)服务模块代码具体错误代码

    对错误码划分区段,利用不同区段表示不同模块,再进行错误编码。这种编码方式的错误码数量会受到一定的限制,例如,10100-10199 使用完时,就不得不占用 102、103开头的错误码。当然,也可以在设计错误码时,预留充足的编码空间。例如:

    第1位第2-4位第5-8位
    20500200

    2.3.2 使用英文短语编码

    常见的系统错误码,都是仅使用阿拉伯数字,例如,Windows 系统中的错误码编码就是从 0000 到 15999 递增。使用数字的好处是处理效率高,容易编码。但是,一个数字能表达的含义有限。如果能使用短语,直接给出错误提示,更加直接有效。

    ERROR_INVALID_FUNCTION
    ERROR_INVALID_FUNCTION
    ERROR_PATH_NOT_FOUND
    ERROR_TOO_MANY_OPEN_FILES
    ERROR_ACCESS_DENIED
    

    在代码层面,使用英文短语编码与数字编码的区别在于

    if (code == "10100")
    if (code == "ERROR_ACCESS_DENIED")
    

    2.3.3 使用状态图编码

    应用系统的本质是一个有限状态机,而一个错误码表示的就是应用系统的一种错误状态。设计错误码,也就是对应用系统状态进行编码。

    以一个简单的购物 Web 系统为例。应用系统只有三个逻辑模块,登录、前置条件检查、付款。

    此时,该应用系统有三个 login、front、pay 三个节点。①②③④⑤六条路径。

    # 路径 - ②⑤
    ERROR_LOGIN_FRONT_NOT_XXX
    # 路径 - ②③④
    ERROR_LOGIN_FRONT_PAY_NOT_XXX
    

    如果新增了一个处理节点,exchange

    # 路径 - ②⑥
    SUCCESS_LOGIN_FRONT_EXCHANGE
    

    通过状态图进行错误编码的好处是,能够非常准确的描述哪里出错,系统扩展时,只需要新增节点和领边。这里当然也可以使用数字进行编码,比如节点 login (100),front(101),路径 - ②⑤ (100101XXX)。

    3. Django 如何处理异常

    Debug = True 时,如果发生异常,Django 将程序运行时的相关信息回显在页面上,方便开发者调试。如下图:

    Debug = False 时,如果发生异常,Django 返回自定义或者内置的 500、404等页面。如下图:

    下面来看下 Django 是如何处理这些异常的:

    3.1 Djang 对 request 的处理

    以本地开发为例,当浏览器发起一次请求时,Django 中的 wsgi 创建一个 WSGIHandler 对象处理请求。在 WSGIHandler 对象中初始化环境变量,如果没有异常,则调用 self.get_response(request) 函数处理请求,返回 response 给 wsgi。

    get_response 定义在 django.core.handlers.base.py 文件中,下面是处理流程。

        for middleware_method in self._request_middleware:
            response = middleware_method(request)
            if response:
                break
        ...
        if response is None:
        ...
            for middleware_method in self._view_middleware:
                response = middleware_method(request, callback, callback_args, callback_kwargs)
                if response:
                    break
        ...
        response = wrapped_callback(request, *callback_args, **callback_kwargs)
        ...
        if response is None:
            try:
                response = wrapped_callback(request, *callback_args, **callback_kwargs)
            except Exception as e:
                for middleware_method in self._exception_middleware:
                    response = middleware_method(request, e)
                    if response:
                        break
                if response is None:
                    raise
        ...
        for middleware_method in self._response_middleware:
            response = middleware_method(request, response)
        ...
        return response
    

    这张图能比较好的呈现整个处理流程逻辑.

    3.2 ExceptionBox

    Django 的中间件支持一种 Exception 的写法。当发生未捕获处理的异常时,执行中间件中定义的函数 process_exception,如果返回一个 response, 那么就可以结束整个流程。

    在 Django 工程中,需要一个异常处理和错误码统一管理的模块。于是便有了 ExceptionBox。

    数据的返回格式:

    {
        'code': 'XXXXXX',
        'message': '错误提示XXXX',
        'result': False,
        'data': None
    }
    

    __init__.py

    # -*- coding: utf-8 -*-
    from .error import *
    

    base.py

    # -*- coding: utf-8 -*-
    from abc import ABCMeta
    
    class BaseReturn(Exception):
        __metaclass__ = ABCMeta
    
    class PreconditionFailed412(BaseReturn):
        status_code = 412
    

    error.py

    # -*- coding: utf-8 -*-
    from __future__ import unicode_literals
    
    from . import base
    
    # Example
    class ERROR_LOGIN_FRONT_NOT_GIFT(base.PreconditionFailed412):
        message = "礼品不充足"
    

    middleware.py

    # -*- coding: utf-8 -*-
    import json
    import logging
    import traceback
    
    from django.http import JsonResponse
    
    from .base import BaseReturn
    
    logger = logging.getLogger('root')
    
    
    class ExceptionBoxMiddleware(object):
        def process_exception(self, request, exception):
            if not issubclass(exception.__class__, BaseReturn):
                return None
            ret_json = {
                'code': exception.__class__.__name__,
                'message': getattr(exception, 'message', 'error'),
                'result': False,
                'data': None
            }
            response = JsonResponse(ret_json)
            response.status_code = getattr(exception, 'status_code', 500)
            _logger = logger.error if response.status_code >= 500 else logger.warning
            _logger('status_code->{status_code}, error_code->{code}, url->{url}, '
                    'method->{method}, param->{param}, '
                    'body->{body},traceback->{traceback}'.format(
                status_code=response.status_code, code=ret_json['code'], url=request.path,
                method=request.method, param=json.dumps(getattr(request, request.method, {})),
                body=request.body, traceback=traceback.format_exc()
            ))
            return response
    

    my_view.py

    import exceptionbox
    def home_view(request):
        raise exceptionbox.ERROR_LOGIN_FRONT_NOT_GIFT()
    

    4. 参考