惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
S
Security Affairs
阮一峰的网络日志
阮一峰的网络日志
L
LangChain Blog
Microsoft Azure Blog
Microsoft Azure Blog
雷峰网
雷峰网
Recent Announcements
Recent Announcements
WordPress大学
WordPress大学
The GitHub Blog
The GitHub Blog
博客园_首页
The Cloudflare Blog
M
MIT News - Artificial intelligence
博客园 - 【当耐特】
MyScale Blog
MyScale Blog
S
SegmentFault 最新的问题
P
Proofpoint News Feed
Y
Y Combinator Blog
Jina AI
Jina AI
博客园 - 聂微东
A
About on SuperTechFans
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
F
Full Disclosure
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Microsoft Security Blog
Microsoft Security Blog
爱范儿
爱范儿
T
Tailwind CSS Blog
J
Java Code Geeks
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
小众软件
小众软件
酷 壳 – CoolShell
酷 壳 – CoolShell
T
The Blog of Author Tim Ferriss
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 三生石上(FineUI控件)
W
WeLiveSecurity
PCI Perspectives
PCI Perspectives
Attack and Defense Labs
Attack and Defense Labs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
宝玉的分享
宝玉的分享
IT之家
IT之家
Hacker News: Ask HN
Hacker News: Ask HN
The Register - Security
The Register - Security
T
The Exploit Database - CXSecurity.com
T
Threat Research - Cisco Blogs

陈少文的网站

巨变与机遇的未来十年 Kubernetes 平台管理软件压力测试方案 使用镜像部署 Hexo 静态页面 终于等到你 - GitHub 镜像仓库服务(ghcr.io) 一起来学 Go --(6)Interface 一起来学 Go --(5)Goroutine 和 Channel 什么是函数式编程 如何在 Kubernetes 集群集成 Kata 柯里化与偏函数 使用 PyGithub 自动创建 Label 软件产品是团队能力的输出 Helm 2 、Helm 3 比较 IoT 变现 Kubernetes 中的 DNS 服务 国内的 Helm 镜像源 Harbor 使用自签证书支持 Https 访问 DevOps 工具链之 Prow 如何使用 kfctl 安装 Kubeflow VS Code 无法下载 Go 插件的工具包 工程师更应具有服务精神 你不知道的 Docker 使用技巧 使用 Docker 运行 Tensorflow 论中国 什么是左移 如何清空 Git 仓库全部历史记录 一禅小和尚 有风吹过厨房 时间的玫瑰 如何在 CentOS 安装 GPU 驱动 开发 Tips(19) 使用 Velero 备份 Kubernetes 集群 Kubernetes Cheat Sheet 开发 Tips(18) 如何构建一个 Java 工程 开发 Tips(17) KubeSpray 安装 Kubernetes 报错 ip in ansible_all_ipv4_addresses 基于 Kubernetes 和 Jenkins 搭建自动化测试系统 在 Kubernetes 上动态创建 Jenkins Slave 使用 Jenkins 进行服务拨测 开发 Tips(16) Kubernetes 签发 Ingress 证书及日常故障运维 Kubernetes 中 Deployment 的基本操作 Kubernetes 中的证书 如何使用 KubeBuilder 开发一个 Operator Kubernetes 1.6.0 安装问题汇总 镜像管理工具 -- Harbor 开发 Tips(15) Docker 如何拉取镜像 开发 Tips(14) 使用 Helm 安装 harbor 开发 Tips(13) 使用 S2I 构建云原生应用 在 Kubernetes 中使用 emptyDir、hostPath、localVolume 开发 Tips(12) 开发 Tips(11) 代码质量分析工具 SonarQube 使用 Kubeadm 安装 Kubernetes 集群 一起来学 Go --(4)常用函数 Kubernetes 中的 Ceph Kubernetes 之 Volumes Kubernetes 之 Labels、Selectors 开发 Tips(10) 开源正在重构商业模式 Kubernetes 之网络 Kubernetes 之 API 使用 Helm 和 Operator 快速部署 Prometheus Kubernetes 复杂有状态应用管理框架 -- Operator Kubernetes 的包管理器 -- Helm 一起来学 Go --(3)Go Modules 如何一步一步地优化博客方案 kubectl 实用指南 Kubernetes 中的基本概念 搭建远程 Kubernetes 开发环境 大公司和小公司的 ToB 思路 开发 Tips(9) Go 入门指南 一起来学 Go --(2)数据与逻辑结构 如何预防 Web 富文本中的 XSS 攻击 django-xss-cleaner 云工作时代 一起来学 Go --(1)背景与特点 SaaS 开发团队的不同阶段 你不知道的 Git 使用技巧 输出既服务 微服务设计 继续奔跑 开发 Tips(8) 从账户安全到二次验证 Django 性能之数据库查询优化 Django 性能之分库分表 敏捷开发之研发流程 打造一致性的团队 开发 Tips(7) Pytest 进阶学习之 Mock PaaS 部署之 buildpack Go 开发配置 领域输出才是 PaaS 的核心竞争力 Pytest 入门学习 开发 Tips(6) 如何使用 Jenkins、Docker、GitLab 搭建 Django 自动化部署流程
错误码设计以及 Django 的异常统一处理
微信公众号 · 2017-10-31 · via 陈少文的网站

笔者目前使用 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

错误码格式

1
2
3
4
5
6
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 递增。使用数字的好处是处理效率高,容易编码。但是,一个数字能表达的含义有限。如果能使用短语,直接给出错误提示,更加直接有效。

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. 参考