使用ASP.NET Core 2.1 HttpClientFactory下载PDF遇TaskCanceledException问题
我来帮你排查这个问题——TaskCanceledException在HttpClient场景下,绝大多数情况和超时、连接中断或者服务器端的请求拦截有关,尤其是你提到只有这个特定URL出问题、wget却能正常下载的情况,大概率是HttpClient的默认配置和archive.org的服务器行为不匹配导致的。下面是几个针对性的解决方案:
延长HttpClient的超时时间并模拟浏览器请求头
HttpClient默认的超时时间是100秒,如果这个PDF文件体积较大,下载时间超过这个阈值就会触发超时异常。另外,有些服务器会拦截默认的HttpClient请求头,建议同时模拟浏览器的User-Agent:services.AddHttpClient("ArchiveOrgDownloader", client => { // 根据文件大小调整超时时间,比如设置为10分钟 client.Timeout = TimeSpan.FromMinutes(10); // 模拟Chrome浏览器的请求头,避免被服务器拦截 client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"); });优化请求的流处理方式
默认的GetByteArrayAsync或GetStringAsync会把整个文件加载到内存中,不仅容易超时,还可能引发内存问题。建议使用HttpCompletionOption.ResponseHeadersRead,拿到响应头后就开始流式写入文件,不用等待整个响应完成:var client = _httpClientFactory.CreateClient("ArchiveOrgDownloader"); var request = new HttpRequestMessage(HttpMethod.Get, "https://archive.org/download/1952-03_IF/1952-03_IF.pdf"); // 只等待响应头返回,就开始处理流 var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); using (var responseStream = await response.Content.ReadAsStreamAsync()) using (var fileStream = new FileStream("IF.pdf", FileMode.Create, FileAccess.Write, FileShare.None, 8192, true)) { await responseStream.CopyToAsync(fileStream); }调整连接池和重定向设置
有些时候archive.org会有多次重定向,或者服务器对并发连接数有限制。你可以配置SocketsHttpHandler来优化这些行为:services.AddHttpClient("ArchiveOrgDownloader") .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { // 增加单服务器的最大连接数 MaxConnectionsPerServer = 10, // 设置连接池的生命周期,避免 stale 连接 PooledConnectionLifetime = TimeSpan.FromMinutes(5), // 允许更多次自动重定向(默认是5次) MaxAutomaticRedirections = 10 }) .ConfigureHttpClient(client => { client.Timeout = TimeSpan.FromMinutes(10); client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"); });尝试分块下载(Range请求)
如果文件确实很大,你可以发送Range请求分块下载,每次只下载一部分内容,降低单次请求的超时风险:var client = _httpClientFactory.CreateClient("ArchiveOrgDownloader"); var fileSize = await GetFileSize(client, "https://archive.org/download/1952-03_IF/1952-03_IF.pdf"); using (var fileStream = new FileStream("IF.pdf", FileMode.Create, FileAccess.Write, FileShare.None)) { long bytesDownloaded = 0; const long chunkSize = 1024 * 1024; // 1MB每块 while (bytesDownloaded < fileSize) { long endOffset = Math.Min(bytesDownloaded + chunkSize - 1, fileSize - 1); var request = new HttpRequestMessage(HttpMethod.Get, "https://archive.org/download/1952-03_IF/1952-03_IF.pdf"); request.Headers.Range = new RangeHeaderValue(bytesDownloaded, endOffset); var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); using (var responseStream = await response.Content.ReadAsStreamAsync()) { await responseStream.CopyToAsync(fileStream); } bytesDownloaded = endOffset + 1; } } // 辅助方法:获取文件总大小 private async Task<long> GetFileSize(HttpClient client, string url) { var request = new HttpRequestMessage(HttpMethod.Head, url); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); return response.Content.Headers.ContentLength ?? 0; }
至于为什么wget能正常工作:wget默认的超时设置更长,并且自带重试机制,同时它的请求头更接近标准浏览器,服务器不会对其进行限制,所以能顺利完成下载。
内容的提问来源于stack exchange,提问作者bitbonk

