XMLHttpRequest和Chrome开发者工具显示的内容不一致

7
我正在使用XMLHttpRequest和Range头以5MB的块下载一个大约50MB的文件,一切都很好,除了检测我何时下载完最后一个块。
以下是第一个块的请求和响应的截图。请注意Content-Length为1024 * 1024 * 5(5MB)。还请注意,服务器正确地响应了前5MB,并在Content-Range标头中正确地指定了整个文件的大小(在“/”之后):
当我将响应正文复制到文本编辑器(Sublime)中时,我只得到了5242736个字符,而不是预期的5242880个字符,如Content-Length所示。
每个下载的块都会缺少144个字符,尽管确切的差异略有不同。
然而,特别奇怪的是最后一个块。服务器响应文件的最后约2.9 MB(而不是整个5 MB),并显然在响应中正确地指示了这一点。
请注意,我正在请求下一个5MB(即使它超出了总文件大小)。没关系,服务器将以文件的最后一部分响应,并且标头指示实际返回的字节范围。
但是它真的吗?
当我使用Javascript调用xhr.getResponseHeader(“Content-Length”)时,在Chrome中看到了一个不同的故事:
XMLHttpRequest对象告诉我已经下载了另外5MB,超出了文件结束。我对xhr对象有什么不理解吗?
更奇怪的是,它在Firefox 30中按预期工作:
因此,在xhr.responseText.length与Content-Length不匹配以及这些标头在xhr对象和Network工具之间不一致之间,我不知道该如何修复它。
是什么导致了这些差异?
更新:我已经确认服务器本身正在正确发送请求,尽管在请求最后一个块时Range头超出了范围。这是原始HTTP请求的输出,感谢好的“telnet”:
HTTP/1.1 206 Partial Content
Server: nginx/1.4.5
Date: Mon, 14 Jul 2014 21:50:06 GMT
Content-Type: application/octet-stream
Content-Length: 2987360
Last-Modified: Sun, 13 Jul 2014 22:05:10 GMT
Connection: keep-alive
ETag: "53c30296-2fd9560"
Content-Range: bytes 47185920-50173279/50173280

看起来Chrome出现了故障。这应该被归档为一个bug吗?在哪里归档?


1
xhr.responseText.length 是响应中字符的数量,而不是 Content-Length 头中指示的字节数。某些 Unicode 字符(或强制转换为 Unicode 的二进制位)每个字符使用多个字节。Chrome 可能会重新考虑无效的范围头(例如与文件结尾重叠的头),Firefox 也可能如此,但只有一种方法(ff)似乎适用于您的情况。修复请求标头,然后再试一次。 - dandavis
感谢 @dandavis。请看我的更新。我直接在 telnet 中运行了请求,服务器的原始输出与预期相同,这意味着(我想?)Chrome 在进行 XMLHttpRequest 时出现了故障或其他问题... - Matt
1
我建议Chrome可能正在执行一些特殊操作,未在此处显示,通过将请求与响应内部绑定。从服务器输出看起来很好,但Chrome可能还考虑了请求的输入(特别是范围0-52/50),这告诉它要期望更多内容。有时候,当你是浏览器时,聪明反被聪明误。 - dandavis
@dandavis 我想我明白你的意思了,Chrome可能会做出一些不应该做出的假设。但是...输入/请求并没有包括总文件大小:只有响应的Content-Range才有这个信息。 - Matt
我的意思是,你在第一张截图中展示的范围请求头(即0-5242879)可能会欺骗Chrome。我不知道为什么你每次都回复响应,但如果是我,我会尝试更改请求…… - dandavis
@dandavis 你可能是对的。无论如何,我已经更改了请求头,使其永远不会超过文件大小,现在它可以工作了。谢谢!不过我会保持问题开放,直到有人给出更具有决定性的答案。 - Matt
1个回答

6
主要问题是您正在将二进制数据读取为文本。请注意,服务器响应的 Content-Type: application/octet-stream 没有明确指定编码 - 在这种情况下,浏览器通常会假定数据以 UTF-8 编码。虽然长度大多数情况下不会改变(值为 0 到 127 的字节在 UTF-8 中被解释为单个字符,并且值较高的字节通常会被替换为替换字符�),但您的二进制文件肯定包含一些有效的多字节 UTF-8 序列 - 这些序列将合并为一个字符。这就解释了为什么 responseText.length 不匹配从服务器接收到的字节数。
现在,您当然可以使用 request.overrideMimeType() 方法 强制使用某些特定的编码,特别是 ISO 8859-1,因为前 256 个 Unicode 码点与 ISO 8859-1 相同:
request.overrideMimeType("application/octet-stream; charset=iso-8859-1");

这样可以确保一个字节始终被解释为一个字符。但更好的方法是将服务器响应存储在ArrayBuffer中,该对象专门用于处理二进制数据。

var request = new XMLHttpRequest();
request.open(...);
request.responseType = "arraybuffer";
request.send();

...

var array = new Uint8Array(request.response);
alert("First byte has value " + array[0]);
alert("Array length is " + array.length);

根据MDN,Chrome 10、Firefox 6和Internet Explorer 10开始支持responseType = "arraybuffer"。另请参见:Typed arrays
附注:火狐浏览器还支持responseType = "moz-chunked-text"responseType = "moz-chunked-arraybuffer",从Firefox 9开始,这使得可以分块接收数据而不必采用范围请求。看起来Chrome没有计划实现它,而是正在实现Streams API

编辑:我无法重现Chrome关于响应头的错误,至少没有你的代码。然而,负责此问题的代码应该是partial_data.cc中的此函数:

// We are making multiple requests to complete the range requested by the user.
// Just assume that everything is fine and say that we are returning what was
// requested.
void PartialData::FixResponseHeaders(HttpResponseHeaders* headers,
                                     bool success) {
  if (truncated_)
    return;

  if (byte_range_.IsValid() && success) {
    headers->UpdateWithNewRange(byte_range_, resource_size_, !sparse_entry_);
    return;
  }

这段代码将删除服务器返回的Content-LengthContent-Range头,并用从请求参数生成的头替换它们。鉴于我无法自己重现此问题,以下只是猜测:
  • 这条代码路径似乎仅用于可以从缓存中满足的请求,因此如果您清除缓存,则猜测事情将正常工作。
  • resource_size_变量在您的情况下必须具有错误值,大于所请求文件的实际大小。该变量是从第一个请求的Content-Range头中确定的,也许您在那里缓存了一个指示较大文件的服务器响应。

感谢详细的解释!但实际上该文件是文本 (.csv),尽管服务器说它是 application/octet-stream。这应该不会有影响,因为文件中没有超过 127 的字节... 我主要关心的是 JavaScript 调用 xhr.getResponseHeader("Content-Length") 返回的值与网络选项卡中显示的 Content-Length 不匹配。这是 Chrome 的一个 bug 吗?(在 FF 中也是一样的。) - Matt
@Matt:根据你的截图,Firefox 显示了正确的内容长度。至于 Chrome,我倾向于不信任调试器。如果你执行 console.log(xhr.getResponseHeader("Content-Length")) 会怎样? - Wladimir Palant
(好的,抱歉,我的意思是FF似乎是正确的。)console.log行在Chrome中显示与调试器相同的(不正确)值。 - Matt
太棒了,感谢您的关注和挖掘Chromium源代码!这些细节可能会帮助我至少创建一个可重现的测试用例。这是非常有帮助的。 - Matt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接