RESTful API弊端

RESTful 是接口架构风格(而非标准), 其核心原则包括资源导向、HTTP 方法语义化、状态码标准化、无状态等;
也就是将接口行为抽象关联成 HTTP 响应方法:

  • GET: 获取数据, 参数以 ?xxx=yyy 方式附加查询
  • POST: 创建数据, 以内部 form/json 结构体方式提交
  • PUT: 更新数据, 和 POST 类似, 但是要求以 ?id=1 或者 /id/1 方式更新指定数据
  • DELETE: 删除数据, 也是针对 ?id=1 或者 /id/1 方式删除数据

RESTful API 请求方法和具体语义挂钩, 将请求地址视为 资源(Resources), 我们需要处理的就是对资源的 增删改查.

但是这种方式仅仅作为 理想状态下, 实际上业务层面的事情复杂得多, 并且还带有其他外部影响.

曾经我也是纯正的 RESTful 原教旨主义, 但是日常经过大量业务积累之后发现很多业务单纯 RESTful 简直是折磨

这里就说明下具体业务场景, 说明下日常可能出现的问题.

网关异常

这是首当其冲的问题, 一般来说正式上线的项目请求流量过大都会购买 “高防服务器” 来做流量清洗和拦截, 之后转发到实际内网服务器.

这部分的网关/代理/中间件当中可能本身基于特殊拦截情况(默认拦截非 200 等特殊响应码), 直接返回自定义异常页面或者信息.

这时候客户端/前端接收到的反而是某些匹配异常错误码但是数据不一致的内容, 比如以下流程:

  1. 前端请求某个接口

  2. 接口代理网关接收到数据发送给内网指定服务

  3. 内网服务返回 404 找不到资源的 JSON 响应(找不到数据按照匹配语义可以返回 404 不存在资源)

  4. 代理网关拦截到 404 异常, 默认的网关认定 404 是自己需要处理的, 返回本身的 404 错误 HTML 页面

  5. 前端接收到这个 HTML 页面, 因为采用 RESTful API 屏蔽底层错误而把他默认认定为 JSON 内容而去解析

  6. 这时候 JSON 解析 HTML 页面直接抛出异常, 而且具体的错误在中间过程当中丢失

这里 404 错误只是列举例子, 实际上这个错误码可能是任何某个网关拦截的异常错误码

而且这是日常最容易碰到的 ‘事故多发地’, 而有时候这些第三方的网关代理不是作为业务设计者能够选择的(这部分是可能是运维处理).

有的清洗网关更加严格, 只允许特定的 HTTP 响应码才能放行, 如果频繁出现非 200 状态码会直接触发告警机制

状态模糊

HTTP 状态返回有时候需要做区分, 方便前端/客户端来获取指定不同的错误消息, 但有时候单纯几个错误码没办法覆盖全部错误.

其中以 订单支付 功能就是问题重灾区, 订单支付流程当中的状态是极其复杂的:

  • 支付参数错误

  • 支付金额异常

  • 支付余额不足

  • 支付通道限额

  • 支付订单已存在

  • 支付订单已取消

这部分错误都是要对应错误状态做对应处理(比如某些状态需要关闭当前支付窗口, 某些状态需要重新跳转指定页面等).

单纯返回 400(Bad Request) 错误并不能标识指定的状态错误, 只能辅助在返回的 JSON 结构之中追加 status 错误码字段.

那么回过头来说, 都在响应内容追加状态那为什么大动干戈采用 RESTful 接口的接口, 统一 200 响应码 + JSON结构体不是更加简洁?

信息泄漏

这也是一直被人隐藏的问题, 那就是查询条件都是采用参数放置在 GET 之中, 如 ?appid=1001

你会感觉其实没什么问题, 但是需要知道大部分情况下 GET 请求都会被网关记录将 路径+查询参数 默认记录在日志.

这时候就意识到问题了, 如果有比较隐私的字段查询(比如唯一的兑换礼包码/身份证号/银行卡号)是否就会落地保存在中间网关的日志?

直接采用 POST 提交参数让网关之类无法捕获 GET 明文参数虽然没有问题, 但是明显违反的 RESTful 规则(GET提取资源,POST创建资源)

可能会觉得网关泄漏是网关的问题, 和 RESTful 没有关系, 但是后端这种配套链路服务就是要规避这种问题的发生而不是互相指责

这是很多人会忽略的点, 像是实际开发业务看到很多私密的字段都这样写入在 Nginx 的 access.log 日志之中.

另外还有点就是查询参数是有限的, 一般默认长度都是为 URL <= 4~8KB, 有的系统查询字段是极其复杂的情况, 这部分可能放不下.

最终总结

在实践当中采用 RESTful 出现太多问题, 最后只能统一采用 HTTP 200 + JSON Payload, 尽可能保持多方面兼容和轻量.

让系统错误去由上层拦截处理, 之后完成返回过来的 HTTP=200 的数据必然是响应 JSON 结构对象, 这部分逻辑更加简洁能够应对更多复杂逻辑.

本质是就是放弃了对协议层语义的执念,聚焦于业务的实际需求, 不再拘泥 RESTful 规则

而抛弃 RESTful 规则之后, 只需要定义类似的 JSON 结构不需要处理花哨的 HTTP 响应码:

lines
1
2
3
4
5
6
7
8
9
{
// 错误码, 可以被拦截并且映射自定义的错误后续处理: 跳转|刷新|回到首页等
"status": 10001,
// 最后备用错误消息, 如果当完全找不到映射错误码消息处理, 则直接采用兜底的错误消息内容
"message": "ERROR",
// 具体成功之后返回的数据列表, 采用 {} 统一定义, 后续内容放置其中 { data:{}, limit: 0, page: 0 }
"data": {
}
}

将数据响应焦距在内容结构上而不用去管理响应的 HTTP 码.