构建 Web 服务时,我们会使用 RESTful API 来实现组件间的通信,特别是在现今前后端分离的技术背景下。REST 是一种基于 HTTP 协议的通信方式,它简单、基于文本、且在各种语言、浏览器及客户端软件中能得到很好的支持。然而,REST 目前并没有一个普遍接受的标准,因此开发者需要自行决定 API 的设计,其中一项决策就是错误处理。比如我们是否应该使用 HTTP 状态码来标识错误?如何返回表单验证的结果等等。以下这篇文章是基于日常使用中的经验总结的一套错误处理流程,供读者们参考。
错误的分类 错误可以分为两种类型:全局错误和本地错误。全局错误包括:请求了一个不存在的 API、无权请求这个 API、数据库连接失败、或其他一些没有预期到的、会终止程序运行的服务端错误。这类错误应该由 Web 框架捕获,无需各个 API 处理。
本地错误则和 API 密切相关,例如表单验证、唯一性检查、或其他可预期的错误。我们需要编写特定代码来捕获这类错误,并抛出一个包含提示信息的全局异常,供 Web 框架捕获并返回给客户端。
例如,Flask 框架就提供了此类全局异常处理机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class BadRequest (Exception ): """将本地错误包装成一个异常实例供抛出""" def __init__ (self, message, status=400 , payload=None ): self.message = message self.status = status self.payload = payload @app.errorhandler(BadRequest ) def handle_bad_request (error ): """捕获 BadRequest 全局异常,序列化为 JSON 并返回 HTTP 400""" payload = dict (error.payload or ()) payload['status' ] = error.status payload['message' ] = error.message return jsonify(payload), 400 @app.route('/person' , methods=['POST' ] ) def person_post (): """创建用户的 API,成功则返回用户 ID""" if not request.form.get('username' ): raise BadRequest('用户名不能为空' , 40001 , { 'ext' : 1 }) return jsonify(last_insert_id=1 )
返回的错误内容 上例中,如果向 /person
API 发送一个 username
为空的请求,会返回以下错误结果:
1 2 3 4 5 6 7 8 HTTP/1.1 400 Bad Request Content-Type: application/json { "status": 40001, "message": "用户名不能为空", "ext": 1 }
它包括以下几个部分:HTTP 状态码、自定义错误码、错误提示、以及额外信息。
正确使用 HTTP 状态码 HTTP 协议中预定义了丰富的状态码,其中 4xx
表示客户端造成的异常,5xx
表示服务端产生的异常。以下是我们在 API 中经常用到的几种状态码:
200
响应结果正常;
400
错误的请求,如用户提交了非法的数据;
401
未授权的请求。在使用 Flask-Login
插件时,如果 API 的路由含有 @login_required
装饰器,当用户没有登录时就会返回这个错误码,而客户端通常会重定向到登录页面;
403
禁止请求;
404
请求的内容不存在;
500
服务器内部错误,通常是未预期到的、不可恢复的服务端异常。
自定义错误码 客户端接收到异常后,可以选择弹出一个全局的错误提示,告知用户请求异常;或者在发起 API 请求的方法内部进行处理,如将表单验证的错误提示展示到各个控件之后。为了实现这一点,我们需要给错误进行编码,如 400
表示通用的全局错误,可直接弹框提示;40001
、40002
则表示这类错误需要单独做处理。
1 2 3 4 5 6 7 8 9 10 11 fetch ().then (response => { if (response.status == 400 ) { response.json ().then (responseJson => { if (responseJson.status == 400 ) { } else if (responseJson.status == 40001 ) { } }) } })
错误详情 有时我们会将表单内所有字段的验证错误信息一并返回给客户端,这时就可以使用 payload
机制:
1 2 3 4 5 6 7 8 { "status" : 40001 , "message" : "表单验证错误" "errors" : [ { "name" : "username" , "error" : "用户名不能为空" }, { "name" : "password" , "error" : "密码不能少于 6 位" } ] }
Fetch API 对于 AJAX 请求,Fetch API 已经逐渐成为业界标准。我们可以将其包装成一个方法,对请求结果进行错误处理。完整的代码可以在 GitHub (链接 )中查看。
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 function request (url, args, form ) { return fetch (url, config) .then (response => { if (response.ok ) { return response.json () } if (response.status === 400 ) { return response.json () .then (responseJson => { if (responseJson.status === 400 ) { alert (responseJson.message ) } throw responseJson }, error => { throw new RequestError (400 ) }) } switch (response.status ) { case 401 : break default : alert ('HTTP Status Code ' + response.status ) } throw new RequestError (response.status ) }, error => { alert (error.message ) throw new RequestError (0 , error.message ) }) }
可以看到,异常发生后,该函数会拒绝(reject)这个 Promise,从而由调用方进一步判断 status
来决定处理方式。以下是使用 MobX + ReactJS 实现的自定义错误处理流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 loginUser = flow (function * loginUser (form ) { this .loading = true try { this .userId = yield request ('/login' , null , form) } finally { this .loading = false } }) login = () => { userStore.loginUser (this .state .form ) .catch (error => { if (error.status === 40001 ) { } }) }
参考资料