接口调通了,文件下下来只有 0 字节,或者打开提示压缩包已损坏。多数情况是拿 GetStringAsync 去接二进制流,编码转换把字节改坏了。

小文件直接拿字节数组

using var client = new HttpClient(); var bytes = await client.GetByteArrayAsync(url); await File.WriteAllBytesAsync(savePath, bytes);

写法短,适合几百 KB 的图片、配置、证书。HttpClient 要复用,放在 static readonly 字段里,每次 new 一个会把本地端口耗光,跑久了就报 socket 耗尽。

大文件走流,不要整个读进内存

using var resp = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); resp.EnsureSuccessStatusCode(); await using var fs = File.Create(savePath); await resp.Content.CopyToAsync(fs);

ResponseHeadersRead 让方法在收到响应头时就返回,body 边收边写。几百兆的安装包这样下,内存曲线是平的。要做进度条就自己读流,用累计字节除以 Content-Length,注意这个头可能为空或者是压缩后长度。

几个反复踩的坑

  • 目标站点校验 User-Agent 或 Referer,补上请求头再试
  • HttpClient 默认 100 秒超时,大文件必须单独设 Timeout
  • 保存目录不存在,File.Create 直接抛异常,先 Directory.CreateDirectory
  • WebClient 已经过时,新代码不必再写

还有一种坑是重定向,短链跳到 CDN 之后方法变了,默认会跟随 302,但认证头会被丢掉,需要显式处理。下载完比对一次字节数,和响应头里的 Content-Length 对上再交给业务。

我的取舍:临时脚本用字节数组,正式服务一律流式加超时和重试,下载失败重试两次比抛异常给用户强。