Node.js 流式处理大文件 HTTP 响应的实践

举报
yd_70403235 发表于 2026/10/24 19:52:28 2026/10/24
【摘要】 介绍 Node.js 处理大文件 HTTP 响应时从一次性读入内存改为流式管道的完整过程,包含下载与转发两个场景的代码示例、背压处理、常见错误和验证结果。

背景

在 Node.js 服务里下载或转发大文件时,最容易踩的坑是把整个响应读进内存。本文结合一次真实排查,介绍如何用流(Stream)正确处理 HTTP 响应体,以及几个容易忽略的细节。

问题现场

服务需要从对象存储拉取几百 MB 的压缩包,再通过 HTTP 接口提供给下游。最初实现用了 axios 默认行为:

const res = await axios.get(url);
fs.writeFileSync(out, res.data);

文件小的时候测试都通过。真实环境出现 800 MB 的包时,Node 进程内存涨到 1.5 GB 后被系统杀掉。原因是 axios 默认把响应体完整读进内存,writeFileSync 又是同步写盘,两个问题叠加放大了内存峰值。

改成流式处理

正确做法是让数据以管道方式流动,而不是一次性落内存。Node.js 的 http/https 模块本身就返回流式响应,配合 fs.createWriteStream 即可:

const fs = require("fs");
const https = require("https");
function download(url, dest) {
return new Promise((resolve, reject) => {
const req = https.get(url, (res) => {
if (res.statusCode !== 200) {
res.resume();
return reject(new Error(`HTTP ${res.statusCode}`));
}
const file = fs.createWriteStream(dest);
res.pipe(file);
file.on("finish", () => file.close(resolve));
file.on("error", reject);
});
req.on("error", reject);
});
}

关键点有三个:res 是可读流,不需要手动拼接 chunk;pipe 会自动处理背压(backpressure),写盘速度跟不上时暂停网络读取;statusCode 不为 200 时调用 res.resume(),避免连接悬挂。

如果项目里已经在用 axios,也可以加一个 responseType 配置:

const res = await axios.get(url, { responseType: "stream" });
res.data.pipe(fs.createWriteStream(dest));

这样 axios 返回的 data 是可读流,而不是 Buffer。

转发场景:两个流之间架管道

另一个常见需求是把上游响应原样转发给客户端,比如做下载代理。直接把上游响应 pipe 给下游响应即可:

app.get("/proxy", async (req, res) => {
const upstream = await axios.get(url, { responseType: "stream" });
res.setHeader("Content-Type", upstream.headers["content-type"]);
res.setHeader("Content-Length", upstream.headers["content-length"]);
upstream.data.pipe(res);
});

要把 Content-Length 从上游透传给下游,否则客户端无法显示进度。如果上游使用了 chunked 编码,则不要设置 Content-Length,让 Node 自动处理。

常见坑

忘记处理错误事件:流中途断开时,如果不监听 error,进程可能崩溃或文件残缺。生产代码里建议用 pipeline 代替 pipe。

错误后不销毁文件流:写盘失败时目标文件可能只写了一半,需要清理临时文件。

忽略 Content-Length 校验:下载完成后对比字节数,能及早发现被截断的响应。

在 async 函数里忘记 await:pipe 是异步的,但很多人当成同步调用,导致后续逻辑在文件写完前执行。

Node 10 之后官方推荐 stream.pipeline,它会自动处理错误传播和流清理:

const { pipeline } = require("stream/promises");
await pipeline(res, fs.createWriteStream(dest));

验证结果

改造成流式后,800 MB 文件的下载内存峰值从 1.5 GB 降到 90 MB 左右,基本就是 TCP 缓冲区和高水位线的开销。同时用 curl 验证了下载文件的 SHA256 与源文件一致,确认没有截断。

小结

处理大文件 HTTP 响应的核心思路就一句话:别让数据在你手里堆积。用 stream 把网络读取、磁盘写入、下游转发串成管道,让每个环节只持有少量缓冲。代码量并没有增加多少,但内存曲线和稳定性完全不同。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。