-
发生时间:2023-12-06
-
问题描述:
- 根据客户的要求,生产时需要把设备 ID 批量上传到客户的 TSP (Telematics Service Provider,汽车远程服务提供商) 系统。
- 为保证通信安全、防止服务接口被恶意攻击,客户的 TSP 系统对 API 进行约束,要求使用 API 时,需要计算一个签名 sign,与其他一些认证信息共同填到 Header 中。
- 我开发完代码后,自测时发现客户的 TSP 系统一直返回 HTTP 状态码 403 以及错误提示信息「系统内部异常」。
-
问题类别:软件开发
-
原因分析:
- 首先怀疑签名计算有误,因为这是整个需求中最复杂的一环。因此在日志里增加调试打印,但没发现异常。
- 接着怀疑 Header 设置有误,因此打印 Header 各字段,发现各字段的首字母都变成大写了——在设置 Header 时本来有些字段是小写的。
- 由于客户的文档里提到过「Header字段的参数全部转换为小写」,因此这里引起我的怀疑。
- 我在 stack overflow 查了一下,得知请求头的字段名字是大小写不敏感的,因此认为这是个无关因素。
- 同时,我潜意识里还有另外一个想法:也许这只是 Go 在打印 map 时才自动将字段首字母打印为大写的,Header 字段在通信时还是会保留小写。
- 由于同事之前使用 Python 实现过同样的功能,于是我决定对比 Python 和 Go 两份代码进行分析。
- 首先,我将同事的 Python 代码从对应仓库中抽离出来单独运行,结果可以正常 POST!
- 然后,我故意将 Python 代码改错,比如删除或乱改签名,这时会报「签名不一致」的错误。这启示我:Go 代码的错误不关签名的事情。
- 接着,我把 request body 的内容删减甚至清空,这时报的错误也跟 Go 代码的错误不一样。 这启示我:Go 代码的错误不关 request body 的事情。
- 那么,只能是 request header 的问题。于是我把重心再次放在检查 Header 里面。
- 继续增加打印信息、对比 Go 与 Python 的运行结果,还是找不到思路。
- 最后,我终于再次怀疑起 Header 字段大小写的问题了。
- 参考 stack overflow 的回答,我自己定义了一个类型为
map[string][]string的 变量,并将其赋值给Request.Header。 - 再次运行,这次终于成功了。原来就是 Go 的
net/http的Request.Header自动将字段首字母的变大写导致的问题!
- 参考 stack overflow 的回答,我自己定义了一个类型为
-
解决方案:自定义一个类型为
map[string][]string的 变量,设置好各字段后,将其赋值给Request.Header。 -
实施结果:给客户的 TSP 系统发送 POST 请求成功。
-
经验总结:
- Go 的
net/http有个奇怪的特性:Request.Header会自动将字段的首字母转为大写。 - HTTP 协议本身定义了请求头的 key 是大小写不敏感的,但现实生活中的服务器未必完全遵守这一定义。(比如客户的 TSP 系统就没遵守)
- 定位问题时,不要轻易放过每一个怀疑点。要「大胆假设,小心求证」。不要焦急,也不要想当然。(比如认为 「Go 在打印 map 时自动将字段首字母打印成大写」就是个很荒谬的想法)
- Go 的
附录:Go 的 net/http/request.go 的 Request 结构体的 Header 字段的部分注释
// HTTP defines that header names are case-insensitive. The
// request parser implements this by using CanonicalHeaderKey,
// making the first character and any characters following a
// hyphen uppercase and the rest lowercase.
//
Header Header
以上注释说明 HTTP 标准定义了 header key 是大小写不敏感的,而 Go 的 net/http 库选择统一将 header key 首字母大写。