- 后端
【免费下载链接】formidable
The most used, flexible, fast and streaming parser for multipart form data. Supports uploading to serverless environments, AWS S3, Azure, GCP or the filesystem. Used in production.
formidable是 Node.js 生态中广泛使用的 multipart/form-data 流式解析库。本文以仓库根目录 CHANGELOG.md 为骨架,逐版本还原从 v1.0.12 到 3.5.4 的关键演进脉络,并对照 src/Formidable.js、src/FormidableError.js、src/parsers/Multipart.js 及test-node/standalone/下的测试用例,讲清每个版本变更背后的实现原理。读完本文,你将掌握 formidable 3.x 的文件大小限制体系、Promise 化解析、目录上传、安全加固措施与错误码体系,并能借助 CHANGELOG 快速定位源码中的对应实现。
版本演进总览
根据 VERSION_NOTES.md 与 CHANGELOG,formidable 的版本线大致可分为三个阶段:
- v1 系列(2010–2018):最初由 Felix Geisendörfer 为 Transloadit 开发,已长期废弃,NPM 上的
formidable@v1不再维护、不回补安全修复。 - v2 系列:代号
formidable@v2/v2-latest,基于 Modern Streams API 重写,引入 Plugins API、filter选项与错误码体系,要求 Node.js >= 10。 - v3 系列:当前主线(
master分支,formidable@latest),全量切换 ES Modules,移除options.multiples,fields/files恒为数组,并持续进行安全加固,要求更高版本 Node.js(README 注明 Node.js >= 20,package.json 的 engines 声明为 >= 14.0.0)。
CHANGELOG 中最新的 3.5.4 版本对应 package.json 中的"version": "3.5.4",说明仓库快照与日志头部一致。整体来看,3.x 的主线节奏是:3.0.0 完成架构断代,3.1.x–3.4.0 补齐功能,3.5.x 聚焦安全与工程化收尾。
3.5.x:安全加固与工程化收尾
3.5.4:修复os.machine兼容性与 CI 矩阵
3.5.4 修复了os.machine破坏部分依赖方的问题(issue #994),并将 Node 16、18、20、22 纳入 CI/CD。os.machine()是 Node 16.4 之后才稳定暴露的 API,若在不支持它的运行时被调用会导致崩溃。仓库内 .github/workflows/main.yml 承载了这套多版本 CI 矩阵,配合 .github/workflows/codeql-analysis.yml 做安全扫描。
3.5.3:ZAST.AI 安全报告与 cuid2 随机名生成
3.5.3 是一次以安全为核心的版本,其要点包括:
- 根据 ZAST.AI 的安全报告修复若干漏洞,重点是随机文件名生成;
- 用久经实战考验的
@paralleldrive/cuid2替代原先基于 hexoid 的命名方案; - 更新失败测试、CI/CD 工作流与 Actions、CodeQL 安全分析 Action、README 与徽章。
随机文件名生成是文件上传解析的关键安全点:若文件名可预测,攻击者可能构造路径冲突甚至目录遍历。当前实现位于 src/Formidable.js:通过cuid2init({ length: 25, fingerprint: ... })初始化一个 25 字符的 CUID2 生成器,fingerprint由NODE_ENV、os.platform()与os.hostname()组合而成;默认命名逻辑_getNewName直接调用createId()生成随机名(src/Formidable.js)。CHANGELOG 强调:仍是 25 字符长度,因此对存量使用者不构成破坏性变更,但更安全、更快。依赖声明见 package.json 中的"@paralleldrive/cuid2": "2.2.2"。
3.5.2 与 3.5.1:打包兼容与解析器防挂起
- 3.5.2修复了 webpack 环境下导入 hexoid 的困难(#982)。该修复发生在 v3 全面 ES Modules 化的背景下,属于打包器兼容性收尾。
- 3.5.1修复了 multipart 解析器的一个核心缺陷:flush 或 fail,绝不挂起(#945)。对应实现是 src/parsers/Multipart.js 的
_flush(done)方法:当流结束时,若正处于HEADER_FIELD_START且index === 0,或正处于PART_DATA且index === this.boundary.length,则补齐partEnd与end回调;否则只要状态不是END,就报malformedMultipart错误。这一点有专门测试佐证:test-node/standalone/multipart_parser.test.js 用残缺的 multipart 报文(--_\r\n--_--\r\n)验证解析器会抛出malformedMultipart而不会无限等待。
3.5.0:双包发布——ESM 与 CommonJS 并存
3.5.0 引入双包(Dual package)支持:既可作为 ES moduleimport,也可作为 CommonJSrequire。仓库的 package.json 通过exports字段同时声明import(指向./src/index.js)与require(指向./dist/index.cjs)两条入口,构建产物由tool/rollup.config.js经pnpm run build-package生成,prepublishOnly钩子保证发布前自动构建。
3.4.0:form.parse的 Promise 化
3.4.0 让form.parse(req)在不传回调时直接返回 Promise,并以[fields, files]数组形式 resolve。这在 src/Formidable.js 的parse方法中实现:当cb缺省时,内部构造一个 Promise,并把 resolve/reject 包进回调;随后所有error/end事件统一走once(dezalgo(cb))(dezalgo与once见 src/utils.js,确保回调至多执行一次且不会同步抛错)。
对应测试 test-node/standalone/promise.test.js 验证了两个方向:成功时promise.then(([fields, files]) => ...)拿到对象;失败时(如故意设置minFileSize: 10 ** 6)Promise 以httpCode = 400、code === errors.smallerThanMinFileSize的FormidableErrorreject。这意味着从 3.4.0 起,await form.parse(req)成为官方推荐写法:
const form = formidable({ uploadDir: __dirname }); const [fields, files] = await form.parse(req);3.3.x:createDirsFromUploads与目录上传
3.3.2 新增options.createDirsFromUploads,配合uploadDir与filename选项即可实现整目录上传(HTML 端使用<input type="file" name="folders" webkitdirectory directory multiple>)。该选项让 formidable 在写入前自动创建目标子目录。
实现位于 src/Formidable.js 的_newFile:当createDirsFromUploads为 true 时,调用createNecessaryDirectoriesAsync(filepath),它基于decorateForceSequential包装fsPromises.mkdir(dirname, { recursive: true })(src/Formidable.js),从而保证并发上传时多个 mkdir 顺序执行、不互相竞争;目录创建失败会抛出cannotCreateDir错误(httpCode 409)。测试 test-node/standalone/createDirsFromUploads.test.js 用filename: () => 'x/y/z.txt'验证了x、x/y、x/y/z.txt三级目录的自动创建。
同一版本还让form.parse成为 async 函数(未提供回调时忽略其返回的 Promise),并新增 e2e 基准测试(benchmark目录),同时被标记为 npmlatest。
3.2.x:构建完整的文件限制体系
3.2.0 是 3.x 中选项语义变化最密集的版本,核心是把“全局总大小”与“单文件大小”彻底分离:
| 选项 | 3.2.0 后的语义 | 默认值(见 src/Formidable.js) |
|---|---|---|
maxFileSize | 每个文件的大小上限 | 200 * 1024 * 1024(200MB) |
maxFiles | 允许上传的文件总数 | Infinity(3.2.0 新增;当前代码默认 1000,见下注) |
maxTotalFileSize | 一批文件的总大小上限,默认等于maxFileSize(向后兼容) | undefined,构造时回落到maxFileSize |
minFileSize | 单文件最小大小 | 1(1 字节) |
allowEmptyFiles | 是否允许空文件 | false |
实现上,src/Formidable.js 的构造函数在maxTotalFileSize未提供时令其等于maxFileSize;_handlePart中分别维护fileSize(单文件)与_totalFileSize(累计),逐块累加并各自校验(src/Formidable.js),对应错误码为biggerThanTotalMaxFileSize(413)、biggerThanMaxFileSize(413)、smallerThanMinFileSize(400)、noEmptyFiles(400)。
注:CHANGELOG 记载 3.2.0 时
maxFiles默认为Infinity;当前仓库代码(3.5.4)中该默认值已调整为1000,与 README 一致,使用时应以实际安装版本为准。
同版本的“错误重命名”指把误命名为maxFileSize的“总大小”错误更正为maxTotalFileSize相关错误,这正是 2.0.0 引入的“code + httpCode 错误体系”的延续(见后文)。
3.2.5:maxFiles超限提前失败
3.2.5 让maxFiles超限时尽早报错,而不是等整个流处理完。当前实现是_setUpMaxFiles(src/Formidable.js):监听fileBegin事件计数,一旦超过maxFiles立即_error(httpCode 413,错误码maxFilesExceeded)。与之对称的_setUpMaxFields(src/Formidable.js)在field事件上做同样的提前拦截。
3.2.4:keepExtensions的一致性修复
3.2.4 统一了文件扩展名清洗规则:扩展名在第一个非法字符处截断。所谓非法字符,指除.、数字和a-Z(含大小写)之外的所有字符。该修复只影响“未使用filename选项且启用了keepExtensions”的场景。
对应实现为 src/Formidable.js 的invalidExtensionChar判断与 src/Formidable.js 的_getExtension:后者支持多段复合扩展名(a.b.c→.b.c,区别于path.extname只取.c),并从首个非法字符处截断、过滤掉纯.的伪扩展名。默认命名路径_getNewName会拼接${name}${this._getExtension(originalFilename)}(src/Formidable.js)。
3.2.3 / 3.2.2 / 3.2.1:事件与资源清理
- 3.2.3:
end事件保证只发射一次。当前_maybeEnd(src/Formidable.js)通过this.ended与_flushing双重条件避免重复触发;测试 test-node/standalone/end-event-emitted-twice.test.js 精确断言end在事件监听与 parse 回调中各只出现一次。 - 3.2.2:纯重构(#801)。
- 3.2.1:出错时不留空文件。此前
_error中file.destroy()(src/Formidable.js)对文件流的销毁可能不及时,src/PersistentFile.js 的destroy()在销毁写流后通过fs.unlink(filepath)延迟清理磁盘文件。
3.1.x:helpers 与错误补充
- 3.1.0新增两个辅助函数:
firstValues与readBooleans。前者从 3.0.0 起“字段恒为数组”的结果中取首个值,且可传入exceptions保留仍需要数组的字段(如<select multiple>),实现见 src/helpers/firstValues.js;后者把 HTML 表单 checkbox 的"on"值转换为布尔值,实现见 src/helpers/readBooleans.js。README 中给出了配套使用示例:firstValues(form, fieldsMultiple, exceptions)之后再readBooleans(fieldsSingle, expectedBooleans)。 - 3.1.1让 JSON 插件支持顶层 JSON 数组、字符串与数字(此前仅支持对象)。
- 3.1.4补上缺失的
pluginFailed错误,并改用显式 Node.js 导入(node:前缀)。 - 3.1.5修复
PersistentFile.toString(),当前实现返回PersistentFile: <newFilename>, Original: <originalFilename>, Path: <filepath>(src/PersistentFile.js)。
3.0.0:ES Modules 与破坏性变更
3.0.0 是 v3 的断代版本,涉及多项破坏性变更:
- 移除
options.multiples(#730)。此前的“单值/多值”开关被废弃,因为 3.x 中fields与files的取值恒为数组。 - 内部改用现代
URLSearchParams解析查询串,随之移除qs与querystring两个依赖。 - 带
[]的字段名不再享受特殊处理(不再自动按数组展开)。 - 全量使用 ES Modules(#727),
package.json的"type": "module"(package.json)即源于此。 options.enabledPlugins必须直接包含插件函数本身,而不是插件名字符串。默认插件集合定义在 src/Formidable.js 的DEFAULT_OPTIONS中,即[octetstream, querystring, multipart, json],均可从 src/plugins/index.js 导入;构造时若enabledPlugins过滤后为空会抛missingPlugin错误(src/Formidable.js)。
2.0.0:里程碑式重构
2.0.0 为 3.x 奠定了几乎所有基础能力,CHANGELOG 中记录的主要变更包括:
- 文件检测逻辑:从“基于文件名”改为“基于是否携带 mimetype”——
_handlePart中if (!part.mimetype)即视为字段(src/Formidable.js)。 - 新增
options.filter:在文件落盘前过滤,返回 false 即跳过该 part;文档明确“不会让 parse 报错”,除非在 filter 内主动form.emit("error")。 - 错误体系化:为多数错误附加
code与httpCode(详见下文错误码表)。 - 大量重命名:
option.hash→options.hashAlgorithm、file.path→file.filepath、file.type→file.mimetype、file.name拆分为file.newFilename与file.originalFilename。这些字段在 src/PersistentFile.js 与 src/VolatileFile.js 的构造函数中均有体现。 - 默认开启目录遍历防护:
_joinDirectoryName(src/Formidable.js)把最终路径path.resolve后与解析过的uploadDir比对,若不在uploadDir + path.sep前缀之内,则回退为defaultInvalidName(默认"invalid-name"),从根上杜绝../../型攻击。 - 新增
fileWriteStreamHandler:自定义落盘目标(AWS S3、Azure Blob、GCS 等)。_newFile检测到该选项时返回VolatileFile(src/VolatileFile.js),把写入动作完全委托给用户提供的 Writable stream;未设置时默认用PersistentFile写本地文件系统。 - 新增
allowEmptyFiles、minFileSize;字段与文件支持数组;接入 Modern Streams API(#531);引入 Plugins API(#545)并把 parser 收敛到src/parsers/;新增自定义命名options.filename(name, ext, part, form)(#591);处理无效文件名;npm 包不再包含测试文件。
插件架构在 src/plugins/multipart.js 中体现得最直观:插件接收(formidable, options),其this即 form 实例,根据Content-Type匹配后初始化对应 parser(multipart 插件还负责从 header 中抽取 boundary,缺失时抛missingMultipartBoundary400 错误)。parsers 统一从 src/parsers/index.js 导出:JSONParser、MultipartParser、OctetStreamParser、QueryStringParser与DummyParser。
v1 时代的历史积累
虽然 v1 已废弃,CHANGELOG 中的早期条目仍揭示了不少沿用至今的设计:
- v1.2.1:
maxFileSize选项确立,默认 200MB;简化 JSON parser 的缓冲逻辑以规避拒绝服务攻击;修复请求中止时的上传文件清理;修复关闭的_writeStream的错误处理。 - v1.1.1:处理
os.tmpDir()弃用警告(改用os.tmpdir(),并沿用为uploadDir默认值);修正buffer.write参数顺序以适配 Node 7;JSON parser 向IncomingForm转发 error 事件;改进Content-Disposition解析;按需在运行时获取fs.WriteStream;hash 写入 JSON。 - v1.0.14:重新启用 hash 计算并补上 hash 测试;重排
IncomingForm#parse。 - v1.0.13:querystring parser 支持
maxKeys控制(默认 1000,匹配当时 Node 行为);空请求处理;hash 的 update 方法存在性检查。 - v1.0.12:aborted 连接发 error;容忍 multipart 中畸形收尾 boundary;忽略 multipart 前导(preamble);支持
application/json、application/octet-stream与 Base64 编码;新增File#toJSON;文件句柄泄漏修复;文档与 benchmark 脚本改进。
这些早期决策——流式解析、JSON/octet-stream 支持、hash 计算、临时目录默认值——直到 3.x 仍是库的支柱能力。
错误码体系:一张表看懂 FormidableError
2.0.0 引入的code + httpCode错误体系在 src/FormidableError.js 中完整定义。FormidableError继承Error,构造参数为(message, internalCode, httpCode = 500)。以下是当前仓库(3.5.4)的全部内部错误码及其典型 httpCode:
| code | 常量名 | 典型 httpCode | 触发场景(源码位置) |
|---|---|---|---|
| 1000 | missingPlugin | 500 | enabledPlugins为空(src/Formidable.js) |
| 1001 | pluginFunction | 500 | .use传入非函数(src/Formidable.js) |
| 1002 | aborted | 500 | 请求中止(src/Formidable.js) |
| 1003 | noParser | 415 | 无匹配 parser(src/Formidable.js) |
| 1004 | uninitializedParser | 500 | parser 未初始化即写入(src/Formidable.js) |
| 1005 | filenameNotString | 500 | originalFilename非字符串(src/Formidable.js) |
| 1006 | maxFieldsSizeExceeded | 413 | 字段总字节超maxFieldsSize(src/Formidable.js) |
| 1007 | maxFieldsExceeded | 413 | 字段数量超maxFields(src/Formidable.js) |
| 1008 | smallerThanMinFileSize | 400 | 文件小于minFileSize(src/Formidable.js) |
| 1009 | biggerThanTotalMaxFileSize | 413 | 累计文件字节超maxTotalFileSize(src/Formidable.js) |
| 1010 | noEmptyFiles | 400 | allowEmptyFiles: false且文件为空(src/Formidable.js) |
| 1011 | missingContentType | 400 | 缺少content-type头(src/Formidable.js) |
| 1012 | malformedMultipart | 400 | multipart 报文畸形/提前结束(src/parsers/Multipart.js) |
| 1013 | missingMultipartBoundary | 400 | multipart 头缺 boundary(src/plugins/multipart.js) |
| 1014 | unknownTransferEncoding | 501 | 未知传输编码(src/plugins/multipart.js) |
| 1015 | maxFilesExceeded | 413 | 文件数量超maxFiles(src/Formidable.js) |
| 1016 | biggerThanMaxFileSize | 413 | 单文件超maxFileSize(src/Formidable.js) |
| 1017 | pluginFailed | 500 | 插件初始化失败(src/Formidable.js,3.1.4 补齐) |
| 1018 | cannotCreateDir | 409 | 自动建目录失败(src/Formidable.js,3.3.2 引入) |
使用时通过import formidable, { errors } from "formidable"引用这些常量,即可在业务层精确区分错误,如err.code === formidableErrors.maxFieldsExceeded。
实战:如何把 CHANGELOG 条目对应到源码
对于升级排查与二次开发,CHANGELOG + 源码的对照方法很有价值。以“3.4.0 之后 parse 返回 Promise”为例:先在 CHANGELOG 找到条目,再到 src/Formidable.js 看parse签名与内部 Promise 分支,最后到 test-node/standalone/promise.test.js 看行为断言。仓库还提供了可直接运行的端到端示例:examples/with-http.js 展示了createDirsFromUploads、filename、filter、allowEmptyFiles等 3.x 选项的组合用法;examples/store-files-on-s3.js 对应fileWriteStreamHandler的流式转发场景。运行测试可使用pnpm run test-node(原生 node:test 套件)与pnpm run test-jest,或一条命令pnpm run test(见 package.json)。
结语
纵观 CHANGELOG,formidable 的演进呈现出清晰的工程脉络:v1 时代确立流式 multipart 解析与 JSON/octet-stream 能力;v2 完成 Modern Streams 重写、插件化架构、错误码体系与目录遍历防护;v3 走向 ES Modules 与双包发布,并以cuid2随机名、maxTotalFileSize限制体系、createDirsFromUploads目录上传等能力补齐安全与体验短板。对于使用者而言,本文梳理的 3.2.x 限制选项、3.4.0 Promise API、3.5.x 安全改动与错误码表,可直接作为升级到formidable@latest的落地参考。
- 后端
【免费下载链接】formidable
The most used, flexible, fast and streaming parser for multipart form data. Supports uploading to serverless environments, AWS S3, Azure, GCP or the filesystem. Used in production.
相关推荐
GitPython 版本演进与安全加固深度解析:从 3.x 变更日志看库的架构演进与防御实践
GitPython 版本演进与安全加固深度解析:从 3.x 变更日志看库的架构演进与防御实践 GitPython 是一个用于与 Git 仓库交互的 Python
版本控制开发工具Paramiko 版本演进全览:从 changelog 看 SSHv2 库的安全加固、算法扩展与 API 演进
Paramiko 版本演进全览:从 changelog 看 SSHv2 库的安全加固、算法扩展与 API 演进 导读 :本文以 Paramiko 官方变更日志
后端网络通信密码学Spaceship Prompt 版本演进全解析:从 4.x 重构到安全加固的 CHANGELOG 深度指南
Spaceship Prompt 版本演进全解析:从 4.x 重构到安全加固的 CHANGELOG 深度指南 导读 本文以仓库根目录的 CHANGELOG.md
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考