侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 154 篇文章
  • 累计收到 7 条评论

206 只发了 10 个字节,长度声明却写 1024

2026-9-30 / 0 评论 / 8 阅读

206 只发了 10 个字节,长度声明却写 1024

视频拖动进度条、下载器断点续传,走的都是 Range 请求。这一段最容易写错的地方是返回 206 时的 Content-Length:规范说的是这条消息封装的字节数,不是整个文件的长度。写成后者,客户端不会报错,它会一直等剩下的数据。

把 RFC 9110 的相关条文和本机跑出来的行为放一起对一遍。

规范怎么定义 Content-Length

8.6.  Content-Length

   The "Content-Length" header field indicates the associated
   representation's data length as a decimal non-negative integer number
   of octets.  When transferring a representation as content, Content-
   Length refers specifically to the amount of data enclosed so that it
   can be used to delimit framing (e.g., Section 6.2 of [HTTP/1.1]).  In
   other cases, Content-Length indicates the selected representation's
   current length, which can be used by recipients to estimate transfer

关键在于「正确封装的数据量」这半句:它先是用来界定消息边界的,然后才是资源的长度。这两个含义在完整响应里数值相同,在部分响应里不同。

本机实测的环境是一个裸 socket 服务端,文件总长 1024 字节,客户端统一请求 `Range: bytes=0-9`。

```text
== 0. 测试用的服务端 ==
  监听 127.0.0.1:58559,文件总长 1024 字节
  客户端请求统一带 Range: bytes=0-9

服务端可以无视 Range

   A server MAY ignore the Range header field.  However, origin servers
   and intermediate caches ought to support byte ranges when possible,
   since they support efficient recovery from partially failed transfers
   and partial retrieval of large representations.

这条给了一个合法退路:不支持范围请求的服务端可以照常回 200 和整个文件。实测里对应的一路是这样:

== 5. /ignore-range:服务端直接无视 Range,回 200 和整个文件 ==
  响应状态行:HTTP/1.1 200 OK
  响应头 Accept-Ranges: none
  响应头 Content-Length: 1024
  客户端收到正文 1024 字节,curl 退出码 0

客户端照收 1024 字节。代价是白传了 1014 字节,拖动进度条时表现成每次都要从头下载。所以「能忽略」和「该忽略」是两件事。

206 里的 Content-Length 指的是本条消息

   A Content-Length header field present in a 206 response indicates the
   number of octets in the content of this message, which is usually not
   the complete length of the selected representation.  Each
   Content-Range header field includes information about the selected

条文把两个头的分工写得很清楚:Content-Length 数的是这条消息的正文,Content-Range 才带完整长度。写对的样子:

== 1. /good:客户端的 Range 被满足,长度写得对 ==
  响应状态行:HTTP/1.1 206 Partial Content
  响应头 Content-Range: bytes 0-9/1024
  响应头 Accept-Ranges: bytes
  响应头 Content-Length: 10
  客户端收到正文 10 字节,curl 退出码 0

206 加上 Content-Range: bytes 0-9/1024,Content-Length 写 10,客户端收 10 字节退出码 0。三个头各自说清一件事,没有歧义。

单段响应必须带 Content-Range

   If a single part is being transferred, the server generating the 206
   response MUST generate a Content-Range header field, describing what
   range of the selected representation is enclosed, and a content
   consisting of the range.  For example:

规范的原例里 Content-Range 和 Content-Length 同时出现,数值不同:区间是 21010-47021,长度是 26012。这两个数不再相等,正是部分响应的常态。

把长度写成整个文件时会怎样

== 2. /claims-whole:Content-Length 写了整个文件的长度,实际只发 10 字节 ==
  响应状态行:HTTP/1.1 206 Partial Content
  响应头 Content-Range: bytes 0-9/1024
  响应头 Accept-Ranges: bytes
  响应头 Content-Length: 1024
  客户端收到正文 10 字节,curl 退出码 18
  curl 的报错:curl: (18) transfer closed with 1014 bytes remaining to read

客户端收到 10 字节正文,然后停在那里等剩下的 1014 字节,直到连接关闭,报 transfer closed with 1014 bytes remaining to read,退出码 18。这一路在真实网络里更常见的形式是超时:如果中间有代理保持着连接不关,客户端会等到自己的超时时间到了才放弃。

不写长度也是一种收尾方式

== 3. /no-length:不写 Content-Length,靠关闭连接收尾 ==
  响应状态行:HTTP/1.1 206 Partial Content
  响应头 Content-Range: bytes 0-9/1024
  响应头 Accept-Ranges: bytes
  客户端收到正文 10 字节,curl 退出码 0

这一路客户端能正常收到 10 字节,因为 HTTP/1.1 允许用关闭连接表示正文结束。代价是连接不能复用,每段请求都要新建一次,而且客户端无法预知要读多久。

两个头互相矛盾时

== 4. /mismatch:Content-Range 说 10 字节,Content-Length 说 100 字节 ==
  响应状态行:HTTP/1.1 206 Partial Content
  响应头 Content-Range: bytes 0-9/1024
  响应头 Accept-Ranges: bytes
  响应头 Content-Length: 100
  客户端收到正文 100 字节,curl 退出码 0

Content-Range 声称区间是 0-9,也就是 10 字节,Content-Length 却写 100。客户端按长度收了 100 字节,退出码 0,没有任何提示。写这段代码的服务端把两个头的来源搞混了,客户端不会替它纠正:长度决定读多少,区间只是描述。

测试这类响应时的一个坑

客户端和服务端放在同一个进程里跑的时候,用同步方式调客户端(同步的 execFile、同步的 curl 封装)会占住事件循环,服务端根本收不到那个请求,看到的现象是超时和零字节。这会被误判成服务端挂死。用异步调用,或者干脆用裸 socket 拼响应、让外部进程当客户端,两边才互不干扰。

什么时候别自己拼这些头

静态文件服务和成熟框架的范围请求实现已经覆盖了 If-Range、多段响应、416 这些分支,自己拼裸响应只适合做实验或者排查线上问题时做对照。真要自己实现,判断依据是这三条的完整性:区间解析出来的起止位置有没有越界、Content-Range 的完整长度是不是资源的真实大小、Content-Length 是不是恰好等于实际写出的字节数。

我的取舍是:业务接口里不做范围请求,交给静态服务;确实需要分片下发的大文件,也只在出口层做一次,不让每个业务处理函数各自拼一遍这些头。

    🤞 分享