0%

python web 异常框架

一,URL路径

  • 前缀

    路由地址以api开头,后跟微服务名、版本及模块名或资源地址,实例:

    https://host:port/api/pdi_metadata/v1/dep

    pdi_metadata表示消息微服务。

    v1表示版本,以字母v开头,后跟数字。dep表示微服务内部的一个模块或者资源地址。

  • 资源地址

    在路径设计中需要遵守下列约定:

    • 资源命名全部小写且易读,可使用连字符(-)或下划线(_)进行分隔
    • 资源的命名需要符合RESTful风格,只有算法资源中可以存在动词,否则只能使用名词
    • 路径部分使用斜杠分隔符(/)来表达层次结构
    • 若同一资源名会出现在资源地址、资源表述、query中,资源名必须一致,即统一使用下划线(_)分隔。

二, 响应状态码

所有API响应遵守HTTP规范,常见的HTTP状态码如下所示。

状态码 描述
1xx 信息状态码
2xx 成功状态码
3xx 重定向状态码
4xx 客户端错误码
5xx 服务器错误码
  1. 请求被正确返回

    其返回参数定义如下:

    参数 类型 是否必需 描述
    code int64 正确状态码
    data obj 响应的请求数据
  2. 请求调用失败,必须返回出错的相信信息,参数定义如下:

    参数 类型 是否必须 描述
    code int64 业务错误码
    message string 业务错误信息,与code一一对应,用于表明code的含义,应尽量简短,抽象,具有概括性和通用性。
    cause string 导致此错误的原因,也可用于给接口调用者提示解决此错误的办法,相同的code可对应不同的cause
    detail obj 错误详细信息,供调用方查看和展示给最终用户的信息

    备注:

    • 前三位为 HTTP标准状态码,中间三位为系统内全局唯一的微服务错误码标识号,后三位为自定义状态码,应尽量抽象,具有概括性和通用性。
    • 对于参数不合法、json格式不对等由客户端调用API代码不对导致的错误,code后3位为000,原因在cause中说明。
    • 对于需要由客户端判断,用于处理UI和具体功能逻辑的错误,code后3位表明具体错误,细节在detail中说明。

三,错误码方案

PDI产品后期需要嵌入到爱数其他的产品中去,所以可以采取动态配置的方案,实现针对不同的产品,使用不同的错误码。

常见的错误码:

  • 400XXX000: 参数不合法、json格式不对等由客户端调用API代码不对导致的错误
  • 500XXX000:服务内部错误
  • 501XXX000:方法为实现

四,Flask 异常方案

Flask内部通过继承HTTPException类来处理异常,同样,我们可以自定义自己的异常基类类(继承自HTTPException),定义好返回的错误码,请求的url,错误原因等。

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
34
35
36
37
class APIException(HTTPException):
code = 500
message = 'sorry, we made a mistake'
error_code = 999
cause = 'just a mistake'
detail = None
dump_json = False # 是否dump成json串。flask_restful.Resource方式需要设置为False,原生方式需要设置为True

def __init__(self, msg=None, code=None, error_code=None, cause=None, header=None, detail=None):
if code:
self.code = code
if error_code:
self.error_code = error_code
if msg:
self.msg = msg
if cause:
self.cause = cause
if detail:
self.detail = detail
super(APIException, self).__init__(msg, None)

def get_body(self, environ=None):
body = dict(
message=self.message,
code=self.error_code,
cause=self.cause
)
if self.detail:
body.update({"detail": self.detail})

data = json.dumps(body) if self.dump_json else body

return data

def get_headers(self, environ=None):
"""Get a list of headers."""
return [('Content-Type', 'application/json')]

定义好基类之后,派生出各种各样的异常类,自由定义各种状态码的错误及对应的错误信息,出现该异常后,抛出异常。比如:

1
2
3
4
class NotFound(APIException):
code = 404
message = 'the resource are not found'
error_code = 1001

目前定义的派生类有:

  • Success
  • DeleteSucess
  • UpdateSucess
  • ServerError
  • ParameterException
  • NotFound
  • AuthFailed
  • Forbidden

在程序中使用。

1
2
3
4
5
6
7
@input_bp.route('/input', methods=['POST'])
def input_m():
d = request.get_json()

if "file_id" not in d or "op_type" not in d:
return ParameterException(message='file_id 和 op_type两个参数不能为空').get_body()
pass

虽然我们可以在可能出错的地方,继承自己的异常类,然后抛出,但是并不是所有的异常我们都能提前预知。比如参数错误等异常,我们可以提前预知并处理好,但是如果出现逻辑问题等提前没法感知的异常,就不是我们能够控制并处理的。所以我们还需要全局捕获所有异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 全局错误AOP处理
@app.errorhandler(Exception)
def framework_error(e):
api_logger.error("error info: %s" % e) # 对错误进行日志记录
if isinstance(e, APIException):
return e
if isinstance(e, HTTPException):
code = e.code
msg = e.description
error_code = 1007
return APIException(msg, code, error_code)
else:
if not app.config['DEBUG']:
return ServerError().get_body()
else:
return e

如此,Flask中出现的所有异常皆可处理了,保证程序的健壮性。co

五, Sanic异常方案

可以理解sanic为flask的升级版,采用协程机制,并发与效率都比flask高,用法基本上与flask一致。

与flask不同的是:

  • 基类继承自HTTPResponse,而不是HTTPException
  • 全局错误AOP处理机制不同,sanic通过添加异常处理函数的方式实现。
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
class MyResponse(HTTPResponse):
code = 500
message = 'sorry, we made a mistake'
error_code = 999
cause = None
detail = None

def __init__(self, message=None, code=None, error_code=None, cause=None, detail=None):
if code:
self.code = code
if error_code:
self.error_code = error_code
if message:
self.message = message
if cause is not None:
self.cause = cause
if detail is not None:
self.detail = detail
super(MyResponse, self).__init__(body=self.get_body(), status=self.code)

def get_body(self):
body = dict(
message=self.message,
code=self.error_code
)

if self.cause is not None:
body.update(dict(cause=self.cause))
if self.detail is not None:
body.update({"detail": self.detail})

return json_dumps(body)

至于各种派生的错误类,与flask一致。

1
2
3
4
5
class Success(MyResponse):
code = 200
message = 'OK'
local_code = "000"
error_code = 99999

全局异常处理。

1
2
3
4
5
6
async def server_error_handler(request, exception):
logger.error(msg=traceback.format_exc()) # 记录错误日志
return ServerError(message=repr(exception), cause=traceback.format_exc())


app.error_handler.add(Exception, server_error_handler)

发生系统异常后,这里统一使用ServerError返回。ServerError是自己定义的继承自MyResponse的异常类。

1
2
3
4
5
class ServerError(MyResponse):
code = 500
message = 'something happen'
local_code = "000"
error_code = 99999