HTTP 100 状态码与Docker虚拟网卡
(2020年10月22日)
HTTP/1.1 100 Continue
HTTP/1.1协议规定了在进行POST/PUT请求时,如果负载较大的话可以在请求头部添加Expect: 100-continue
然后现将头部发送给服务器。服务器收到此头部后需要决定是否接收后续内容,如果接收则返回100 Continue
,否则返回4xx
错误码,并在全部接收后返回最终响应。
对于curl
,添加该头部的负载大小阈值原先为1KiB,现在为1MiB,相关的讨论与改动如下:
- Time to disable "Expect: 100-continue" by default?
- #3524 Make default EXPECT_100_THRESHOLD 1Mb
- #4814 Make default EXPECT_100_THRESHOLD 1Mb
通过构造一个简单的POST请求可以触发该行为:
$ dd if=/dev/zero of=file bs=1000K count=1
$ curl -v -F "FILE=@file" http://{endpoint}
如果网络正常,会看到curl
只发送了一次请求,却接收到了两次响应:
> POST {endpoint} HTTP/1.1
> Expect: 100-continue
< HTTP/1.1 100 Continue
* Done waiting for 100-continue
* We are completely uploaded and fine
< HTTP/1.1 200 OK
< Content-Length: ...
对于大多数web框架,框架会自动且处理这样的请求,即直接拒绝或先返回100 Continue
然后接收数据、返回最终状态,无需用户手动处理。
如果web服务没有按照HTTP/1.1协议正确实现,即并不返回100 Continue
而是直接等待数据,curl
默认会在等待100状态1000ms超时后直接传输整个请求。同时如果发送的HTTP请求头部被直接拒绝,curl
会直接重传完整的请求。
使用参数-H "Expect:"
可以覆盖掉curl
的默认处理方式,避免该头部导致服务端的错误处理。
Docker for Mac
在Docker for Mac 2.4.0.0下,创建一个Linux容器并使用curl
发送上述请求到ASP.NET Core后端,会发现curl
陷入死循环,不会打印任何内容。strace
调试发现curl
接收的数据为:
HTTP/1.1 100 Continue\r\n
content-length: 0\r\n
而不是ASP.NET Core真正返回的
HTTP/1.1 100 Continue\r\n\r\n
通过Wireshark抓包发现一个HTTP请求收到了两次响应,但docker容器内部只收到了第一次的数据且内容发生了变化,可以推测Docker for Mac使用的虚拟网卡(vpnkit)对数据进行了错误修改和抛弃。
通过粗略阅读了解到,Docker for Mac的网络桥接是通过共享内存队列和virtio
来实现虚拟网卡挂载到guest的eth0
来实现的。容器发送请求时会通过vpnkit将不同类型的数据包转换成Unix的套接字请求发送,并将结果转换成对应的数据写回容器。推测在处理HTTP请求时将请求与响应配对,即一次请求只允许一次响应,导致将100 Continue
认为是请求对应的响应(但其实收到这个数据包时请求还没发送完),且抛弃了之后的其他响应,导致curl
在VM内部卡死。
解决此问题的方案:
- 使用选项
-H "Expect:"
覆盖掉默认的头部 - 使用HTTP/1.0、HTTP/2、HTTP/3、HTTPS协议
- 扔掉MacBook(误)提issue给docker/for-mac然后等待修复