.NET HttpClient - 当响应标头的内容长度不正确时接受部分响应

Posted

技术标签:

【中文标题】.NET HttpClient - 当响应标头的内容长度不正确时接受部分响应【英文标题】:.NET HttpClient - Accept partial response when response header has an incorrect Content-Length 【发布时间】:2020-01-04 10:53:46 【问题描述】:

我正在使用 .NET Core 3.1 开发 ASP.NET Web 应用程序。应用程序从外部网络服务器下载 mp3 文件,该服务器存在错误:响应标头中的 Content-Length 报告的字节数高于 mp3 的实际字节数。

这是一个使用 curl 从该服务器下载文件的示例:

curl -sSL -D - "http://example.com/test.mp3" -o /dev/null
HTTP/1.1 200 OK
Cache-Control: private
Pragma: no-cache
Content-Length: 50561024
Content-Type: audio/mpeg
Content-Range: bytes 0-50561023/50561024
Expires: 0
Accept-Ranges: 0-50561023
Server: Microsoft-IIS/10.0
Content-Transfer-Encoding: binary
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Fri, 03 Jan 2020 23:43:54 GMT

curl: (18) transfer closed with 266240 bytes remaining to read

因此,即使 curl 报告传输不完整,mp3 仍以 50294784 字节完全下载,我可以在任何我尝试过的音频播放器中打开它。

我想要在我的 Web 应用程序中实现与 curl 相同的行为:忽略不正确的 Content-Length 并下载 mp3,直到服务器关闭传输。

现在我只是使用 HttpClient 来异步下载 mp3:

internal static HttpClient httpClient = new HttpClient()  Timeout = new TimeSpan( 0, 15, 0 ) ;
using( var response = await httpClient.GetAsync( downloadableMp3.Uri, HttpCompletionOption.ResponseContentRead ) )
using( var streamToReadFrom = await response.Content.ReadAsStreamAsync() )

但是,与 curl 不同的是,当传输过早关闭时,传输会整体中止:

Task <SchedulerTaskWrapper FAILED System.Net.Http.HttpRequestException: Error while copying content to a stream.
 ---> System.IO.IOException: The response ended prematurely.
   at System.Net.Http.HttpConnection.FillAsync()
   at System.Net.Http.HttpConnection.CopyToContentLengthAsync(Stream destination, UInt64 length, Int32 bufferSize, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnection.ContentLengthReadStream.CompleteCopyToAsync(Task copyTask, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionResponseContent.SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)
   at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
   at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)

有什么方法可以配置 HttpClient 以“忽略”不正确的 Content-Length 并获取 mp3?

【问题讨论】:

【参考方案1】:

如果您查看 dotnet 运行时 repo 中的方法 SendAsyncCore,您会看到相当大的代码实现了发送请求和处理响应的核心功能。如果服务器发送内容长度标头,则此方法在内部创建ContentLengthReadStream。此流需要固定数量的字节,并且会一直读取,直到达到预期数量。如果内容长度大于实际字节数,则ContentLengthReadStream 将引发异常并显示消息The response ended prematurely

由于所有这些方法都非常僵化和内部化,因此没有扩展或更改此功能的空间。但是有一个解决方法。您可以手动将流读入缓冲区,直到抛出异常。流的正常终止条件是 Read 方法返回零字节。如果 content-length 正确,也应包括此条件。

using var resp = await httpClient.GetAsync("http://example.com/test.mp3", HttpCompletionOption.ResponseHeadersRead);
using var contentStream = await resp.Content.ReadAsStreamAsync();

var bufferSize = 2048;
var buffer = new byte[bufferSize];
var result = new List<byte>();

try

    var readBytes = 0;
    while ((readBytes = contentStream.Read(buffer)) != 0)
    
        for (int i = 0; i < readBytes; i++)
        
            result.Add(buffer[i]);
        
    

catch (IOException ex)

    if (!ex.Message.StartsWith("The response ended prematurely"))
    
        throw;
    

上面的代码将整个响应字节加载到列表result中。对于大型内容,这可能不是一个好的解决方案。

还请注意,在这种情况下您不应使用HttpCompletionOption.ResponseContentRead,因为如果您调用GetAsync 方法,它会尝试立即读取内容。由于我们要稍后阅读内容,因此应将其更改为 HttpCompletionOption.ResponseHeadersRead。这意味着GetAsync在读取标题时完成操作(而内容尚未读取)。

【讨论】:

谢谢,这很有魅力。我唯一不同的是直接将字节写入目标文件。

以上是关于.NET HttpClient - 当响应标头的内容长度不正确时接受部分响应的主要内容,如果未能解决你的问题,请参考以下文章

System.Net.Http.HttpClient 添加 Request-Id 标头

如何使 HttpClient 忽略 Content-Length 标头

Angular 6获取带有httpclient问题的响应标头

Angular 4.3.3 HttpClient:如何从响应的标头中获取值?

Angular 7 HttpClient 发布响应标头为空

如何删除 HttpClient 请求标头 C# 中的默认字符集