← 中文资源

简体中文版本

Busboy 流式分段上传限额

用 Busboy 流式解析 multipart 请求体:文件数、体积、类型三重限额在字节落盘前生效,字段截断防超长注入。

阅读英文原文

示例说明

multipart 解析的安全要点在时机:限额要在数据流动过程中生效,而不是收完再检查。Busboy 的 limits 让超限流被直接截断;文件事件里先验 MIME 类型再接盘,不合规的流要 resume 掉——不消费流的解析器会停滞,这是流式处理最容易踩的坑。字段值截断则防御超长键值对撑爆内存。

技术要点

  • limits 在流式解析中即时生效
  • 拒绝的流必须消费防止解析停滞
  • 字段值截断防御超长输入

代码解读

handleUpload 返回 Promise 汇总文件与字段:file 事件按白名单校验类型,合法的接入磁盘写流管道;field 事件把值截到五百字符存入字段表;close 事件表示解析完毕可以 resolve。req.pipe(parser) 把请求体导入解析器启动全流程。

使用建议

文件名要做穿越清洗或直接改名存储。读取的字节数应来自流结束后的统计而不是缓冲长度。大文件上传配进度与断点续传体验更佳。

代码示例

import busboy from 'busboy';
import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import type { IncomingMessage } from 'node:http';

const LIMITS = {
  fileSize: 10 * 1024 * 1024,
  files: 2,
  fields: 12,
};

const ALLOWED_TYPES = /^(image\/png|image\/webp|application\/pdf)$/;

export function handleUpload(req: IncomingMessage): Promise<{
  files: { name: string; bytes: number }[];
  fields: Record<string, string>;
}> {
  return new Promise((resolve, reject) => {
    const parser = busboy({
      headers: req.headers,
      limits: LIMITS,
    });

    const files: { name: string; bytes: number }[] = [];
    const fields: Record<string, string> = {};

    parser.on('file', (name, stream, info) => {
      if (!ALLOWED_TYPES.test(info.mimeType)) {
        stream.resume(); // 必须消费掉流,否则解析停滞
        reject(new Error('unsupported type: ' + info.mimeType));
        return;
      }
      const target = createWriteStream('uploads/' + info.filename);
      pipeline(stream, target).catch(reject);
      files.push({ name: info.filename, bytes: stream.readableLength });
    });

    parser.on('field', (name, value) => {
      fields[name] = value.slice(0, 500);
    });

    parser.on('close', () => resolve({ files, fields }));
    parser.on('error', reject);

    req.pipe(parser);
  });
}