Please enable Javascript to view the contents

错误码设计以及 Django 的异常统一处理

 ·  ☕ 9 分钟

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

1. 错误码的用途

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

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

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

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

状态码 范围 含义
1xx 100-101 指示信息–表示请求已接收,继续处理
2xx 200-206 成功–表示请求已被成功接收、理解、接受
3xx 300-305 重定向–信息不完整需要进一步补充
4xx 400-415 客户端错误–请求有语法错误或请求无法实现
5xx 500-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

错误码格式

1
2
3
4
5
6
JSON
{
	"request" : "/statuses/home_timeline.json",
	"error_code" : "20502",
	"error" : "Need you follow uid."
}

错误代码说明,以 20502 为例

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

部分错误码:

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

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

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

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

错误代码 错误信息 详细描述
40001 invalid credential 不合法的调用凭证
40008 invalid message type 不合法的message_type
40016 invalid 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位
2 05 02
服务级错误(1为系统级错误) 服务模块代码 具体错误代码

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

第1位 第2-4位 第5-8位
2 050 0200

2.3.2 使用英文短语编码

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

1
2
3
4
5
ERROR_INVALID_FUNCTION
ERROR_INVALID_FUNCTION
ERROR_PATH_NOT_FOUND
ERROR_TOO_MANY_OPEN_FILES
ERROR_ACCESS_DENIED

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

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

2.3.3 使用状态图编码

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

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

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

1
2
3
4
# 路径 - ②⑤
ERROR_LOGIN_FRONT_NOT_XXX
# 路径 - ②③④
ERROR_LOGIN_FRONT_PAY_NOT_XXX

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

1
2
# 路径 - ②⑥
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 文件中,下面是处理流程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
	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。

数据的返回格式:

1
2
3
4
5
6
{
	'code': 'XXXXXX',
	'message': '错误提示XXXX',
	'result': False,
	'data': None
}

__init__.py

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

base.py

1
2
3
4
5
6
7
8
# -*- coding: utf-8 -*-
from abc import ABCMeta

class BaseReturn(Exception):
    __metaclass__ = ABCMeta

class PreconditionFailed412(BaseReturn):
    status_code = 412

error.py

1
2
3
4
5
6
7
8
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from . import base

# Example
class ERROR_LOGIN_FRONT_NOT_GIFT(base.PreconditionFailed412):
    message = "礼品不充足"

middleware.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# -*- 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

1
2
3
import exceptionbox
def home_view(request):
    raise exceptionbox.ERROR_LOGIN_FRONT_NOT_GIFT()

4. 参考


微信公众号
作者
微信公众号