张天昀的个人博客

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,相关的讨论与改动如下:

通过构造一个简单的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

通过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然后等待修复