GridFS流式下载需四步:先查元数据获ObjectId,再手动解析Range头,三按chunk序号裁剪读取,四用Readable流控+前端getReader消费;缺一不可。

openDownloadStream 必须传 ObjectId,不能传字符串或 null
直接调用 bucket.openDownloadStream("video.mp4") 会返回空流或抛出 TypeError: Cannot read property '_id' of null。GridFS 驱动不接受文件名字符串,只认 ObjectId 或已查出的完整文档。
正确流程必须分两步:
- 先用
bucket.findOne({ filename: "video.mp4" })查元数据,确保结果存在且非null - 再用
bucket.openDownloadStream(doc._id)开启流,doc._id是ObjectId类型 - 若文件名含中文、空格或大小写混用,查询时需严格匹配——MongoDB 默认区分大小写且不自动 trim
Range 请求必须手动解析,GridFS 本身不识别 HTTP 头
bucket.openDownloadStream() 返回的是完整文件流,不会响应 Range 请求。如果前端发 Range: bytes=0-1048575(即 1MB),后端却直接 .pipe(res),等于全量传输,完全违背“按需”本意。
实现精准分片需手动处理:
- 从
req.headers.range解析起始偏移start和结束偏移end;若末尾未指定(如bytes=500000-),用fileDoc.length补全 - 根据
chunkSizeBytes(推荐设为 262144,即 256KB)计算覆盖该 range 的所有 chunk 序号n - 用
db.fs.chunks.find({ files_id: doc._id, n: { $in: [n1, n2, ...] } }).sort({ n: 1 })按序读取,并对首尾 chunk 做字节裁剪 - 用
stream.Readable手动push()裁剪后的Buffer,避免 accumulate 全量内存
流控不当会导致 OOM,highWaterMark 不是可选项
直接 downloadStream.read() 或监听 data 事件却不控制节奏,Node.js 会持续预加载 chunks 到内存,大视频极易触发 OOM。这不是驱动 bug,是流消费失衡。
安全做法是显式约束缓冲水位:
- 设
downloadStream.readableHighWaterMark = 2 * 1024 * 1024(2MB),匹配典型视频 GOP 大小 - 优先走
pipeline()或.pipe(res),它们天然支持背压;但必须监听res.on('close')清理流 - 避免
stream.on('data', buf => { /* 异步处理 */ })后不pause()/resume(),这会打破背压链 - 检查
fs.chunks中每个data字段长度是否接近你设的chunkSizeBytes,用db.fs.chunks.find({ files_id: ObjectId(...) }).sort({ n: 1 }).limit(5)快速验证
前端用 blob() 就等于放弃流式,必须用 getReader
即使后端正确返回 206 Partial Content 和精确字节段,response.blob() 仍会阻塞等待流关闭,对 GB 级模型或小时级视频毫无意义。
真实流式消费方式是:
- 用
const reader = response.body.getReader()获取读取器 - 循环调用
reader.read().then(({ done, value }) => { ... }),每次拿到Uint8Array - 直接喂给
MediaSource(视频)、WebAssembly.Memory(模型权重)或自定义解码器 - 绝对不要调
response.arrayBuffer()或response.text(),它们隐式等待流终止 - 若需临时 URL,每块用
new Blob([value], { type: "video/mp4" })构造,再URL.createObjectURL(),而非等全部数据
chunkSizeBytes 对齐、手动 Range 解析、流控水位、前端 reader 消费——这四点漏掉任一,所谓“流式读取”就只是名义上的。真正难点不在代码行数,而在每个环节都得和默认行为对抗。


















