• 发生时间: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/httpRequest.Header 自动将字段首字母的变大写导致的问题!
  • 解决方案:自定义一个类型为 map[string][]string 的 变量,设置好各字段后,将其赋值给 Request.Header

  • 实施结果:给客户的 TSP 系统发送 POST 请求成功。

  • 经验总结:

    • Go 的 net/http 有个奇怪的特性:Request.Header 会自动将字段的首字母转为大写。
    • HTTP 协议本身定义了请求头的 key 是大小写不敏感的,但现实生活中的服务器未必完全遵守这一定义。(比如客户的 TSP 系统就没遵守)
    • 定位问题时,不要轻易放过每一个怀疑点。要「大胆假设,小心求证」。不要焦急,也不要想当然。(比如认为 「Go 在打印 map 时自动将字段首字母打印成大写」就是个很荒谬的想法)

附录:Go 的 net/http/request.goRequest 结构体的 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 首字母大写。