示例说明
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);
});
}