☰
formidable 版本演进深度解析:从 CHANGELOG 看 multipart 表单解析库的 3.x 安全加固与 API 升级
2026/10/15 18:48:47 网站建设 项目流程
  • 后端

【免费下载链接】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.

项目地址:https://gitcode.com/gh_mirrors/fo/formidable
点击查看免费下载

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触发场景(源码位置)
1000missingPlugin500enabledPlugins为空(src/Formidable.js)
1001pluginFunction500.use传入非函数(src/Formidable.js)
1002aborted500请求中止(src/Formidable.js)
1003noParser415无匹配 parser(src/Formidable.js)
1004uninitializedParser500parser 未初始化即写入(src/Formidable.js)
1005filenameNotString500originalFilename非字符串(src/Formidable.js)
1006maxFieldsSizeExceeded413字段总字节超maxFieldsSize(src/Formidable.js)
1007maxFieldsExceeded413字段数量超maxFields(src/Formidable.js)
1008smallerThanMinFileSize400文件小于minFileSize(src/Formidable.js)
1009biggerThanTotalMaxFileSize413累计文件字节超maxTotalFileSize(src/Formidable.js)
1010noEmptyFiles400allowEmptyFiles: false且文件为空(src/Formidable.js)
1011missingContentType400缺少content-type头(src/Formidable.js)
1012malformedMultipart400multipart 报文畸形/提前结束(src/parsers/Multipart.js)
1013missingMultipartBoundary400multipart 头缺 boundary(src/plugins/multipart.js)
1014unknownTransferEncoding501未知传输编码(src/plugins/multipart.js)
1015maxFilesExceeded413文件数量超maxFiles(src/Formidable.js)
1016biggerThanMaxFileSize413单文件超maxFileSize(src/Formidable.js)
1017pluginFailed500插件初始化失败(src/Formidable.js,3.1.4 补齐)
1018cannotCreateDir409自动建目录失败(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.

项目地址:https://gitcode.com/gh_mirrors/fo/formidable
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询