记一次解决 Feign 提交批量添加请求收到 400 报错的经历

上周在实现用 OpenFeign 提交批量添加请求时,遇到一个奇怪的问题,本来 Post 请求的 body 体,因为是批量请求,内容就比较多,准备起来就比较麻烦,仔细检查了很多次,提交时还是各种失败,最终终于按照官方文档在 Postman 中提交成功了,本以为万事大吉了,于是开始写 Java 代码, 没想到用 Java 代码用几乎一样的 body 体提交,居然总是失败,收到一个 400 的报错,反复查看那个报错,也看不出什么有用的信息,在网上搜索答案也没有什么提示,甚至考虑是不是 feign 的 client 有什么问题? 中间还尝试了用 ApacheHttpClient 替换默认的 DefaultHttpClient 等,都是一样的问题,卡住了一整天……

第二天恢复精力后,中间有空的时候,考虑是不是自己手拼的 body 体不标准,不确定哪里是不是有什么遗漏, 因为之前就因为少个换行啊什么的出现过一些提交报错,所以专门为此写了生成 body 体的 Java 类,用 TDD 的方式,一点点组装出 Get 的 body 体,想着先从批量 Get 请求开始,稳扎稳打地实现代码。

当天下午,把 Get 请求的 body 体拼接好,执行后居然成功了,非常高兴,以为是之前太粗心,心想还是要稳稳地用单元测试来保证代码的准确啊!

于是继续完成 Post 的 body 体的拼接,以为这样就应该没问题了,没想到,同样的错误又出现了,批量 Get 请求可以,而 Post 请求就失败。 但是为什么 Postman 可以呢? 仔细比对 Postman 和 Java 中的两个 body 体,也实在看不出有啥不同,甚至用 Java 中的 body 体放到 Postman 中提交也是可以成功的。又陷入了僵局,百思不得其解,只好先忙其他的工作。

因为这个工作有一定的时间限制,原来的代码是在 for 循环中一条一条地提交插入数据, 考虑未来运行期的性能问题,想要用批量插入来实现,这样一次提交就可以完成原来要十几个请求才能完成的插入数据功能。可是现在遇到这样的问题,几乎想要放弃了,暂时先用原来的 for 循环解决业务需求。

然后回头想了一下,既然 Postman 都提交成功了,那感觉胜利就近在眼前了,实在是不想放弃。

于是周六休息,放空了一下大脑,没有再想这个事,今天周日中午睡了一觉,看了会儿书后,重振旗鼓,再来尝试解决这个问题。

因为有之前打下的坚实基础, TDD 驱动出来的代码,过程中也重构得很整洁,再次开始,只需要执行之前写好的单元测试,就可以重现问题了,这次使用 debug 一点点深入到 feign 的源码内部,感谢 IDEA Intellij 的强大功能, 一层层进入到 RequestTemplate.java 这个文件, 发现 header 里面多了一项: Content-Length,因为我在提交的请求头中并没有加这一项,因此这个 header 引起了我的注意。

反复调试了几次,发现这个 header 是在 feign 解析请求模板时拼接进来的,当时看到的数字是 729,看到代码里有一些转编码的操作,怀疑是不是编码问题算错了长度?于是把请求的 body 在 Java 里写了一个临时的单元测试,验证了一下长度,果然也是 729 的长度, 把同样的 body 放在 Postman 中提交是可以成功的,但是加上 Content-Length:729 后,就失败了,跟 Java 里遇到的问题一模一样, 哈哈…… 虽然失败了,但很高兴,毕竟是在 Postman 中重现了问题。这样我就在想,是不是这个 multipart/mixed 的 POST 请求不能添加 Content-Length 这个 Header? 目前在 Postman 上测试的情况就是不加这个 header 就可以提交成功,但加上后不管输入什么数字就都是失败。

于是,想了一下似乎没什么办法绕过 feign 的功能,翻了下 OpenFeign feign-core 的源代码,就是在 RequestTemplate.java 这个文件中看到自动添加 Content-Length 这一项的代码,在 feign 的 Github Issue 中也没找到类似的问题,考虑是不是给 feign 开源社区提个 Issue ?于是发了这个 Issue 1810, 然后又想,既然已经找到具体的代码了,为什么不帮忙修改一下提一个 pull request 呢? 于是 Fork 了代码,下来后看到人家的代码也是有配套的单元测试的,很容易测试,不一会儿就把代码修改并测试通过了,提交了 pull request 1811,蛮有成就感地去吃晚饭了。

吃完饭,又在想等人家审核代码、合并发布也要等不知道多少天,我能不能有个临时的解决方案先绕过它呢?是不是我传个什么特殊的值, feign 就不会再加这个 header 了呢? 一些想法都需要验证,好在用 Postman 来推敲问题就相对快捷了, 填 0 ,就直接返回了一个 --batch_xxxx--, 数据库也没有插入任何数据,似乎是直接结束了请求, 填 null 直接报错,非法的值, 填一个很大的数字 1000,就是一直在发送,点取消才能停止,按字符串长度 729 提交,就跟之前的报错一样,然后一点点添加长度,730 往上升,780 往下降,最后在 750 的时候, 提交成功了!

这说明什么? 说明不是不能加 Content-Length 这个值,而是一个特定的值就能成功,可是明明字符串长度是 729,为什么会在 750 时提交成功呢? 再次 debug 到 RequestTemplate.java 中,仔细查看了计算长度的代码,甚至将相同的代码摘出来,单独测试了一下,源代码中虽然是计算的 byte 的长度,但是使用 UTF-8 的编码得到的长度也是 729,难道说 Postman 计算长度时加了别的什么东西?729 和 750 相差了 21,其他 header 似乎也没有什么内容恰好是 21 个字符的, 正在没什么头绪的时候,突然注意到 body 的内容正好是 22 行,很接近 21 这个数字,突然想到,是不是换行符的问题? 22 行内容正好是 21 个换行符。

没错,肯定是这个问题!

于是换了一下换行符,好在之前的代码很整洁,还专门抽了一个常量: String EOL = "\n"; ,所以很简单,改成 "\r\n" 就成了,一试,果然通了!

高兴之余,想到之前提的 pull request 不是错了,赶紧上去关闭了 Issue 和 Pull request。 然后回到项目上的代码,把自己之前写的单元测试改好,测试全通过,代码整理了一下,提交了代码。

回想了一下这个过程,其实好多次都已经接近真相了,可就是没有触及答案,就好像在黑暗中摸索,几次手已经划过了电灯开关附近,但偏偏就是没摸到开关。反思了一下,一方面是自己的经验不足,对 feign 还比较陌生,另一方面,对于 HTTP 请求的细节根本没有深入研究过,对于 multipart/mixed 的请求也是头一次接触,否则可能早就找到问题原因解决了。 好在这样的一次经历最后有了一个圆满的结局,同时也学到了一些新知识。