Summary
fileSizeLimit does not bound memory for multipart bodies, because it is validated after the entire request body has already been buffered and concatenated. A single in-flight multipart/form-data request under the default limits can pin hundreds of MB of server memory.
Detail (at HEAD, v5.1.1)
The shared accumulator in p() pushes every chunk into an in-memory array and only guards against payloadLimit (src/index.ts):
req.on('data', (chunk) => {
totalSize += chunk.byteLength
if (totalSize > payloadLimit) { … } // multipart passes payloadLimit = Number.POSITIVE_INFINITY
else body.push(chunk) // unbounded
})
req.on('end', () => resolve(fn(Buffer.concat(body), req))) // full concat, then td.decode → whole body as a string
multipart() explicitly sets payloadLimit = Number.POSITIVE_INFINITY, so that guard never fires. The only multipart cap, fileSizeLimit (default 200 MiB), is checked inside parseMultipart, after Buffer.concat + td.decode (src/index.ts ~line 159: if (data.length > fileSizeLimit) throw …). So it validates after the memory is already consumed — it is a post-hoc size check, not a memory guard.
Measured with a plain app.use(multipart()) server (Node 24): a 150 MiB body — under both default limits — returns 200 while server RSS grows ~+460 MiB and stays there; N concurrent such requests OOM the process. (400/600 MiB bodies do hit fileSizeLimit/V8 string-length, but only after buffering.)
Note: #22 added request limits to the other parsers but multipart kept Infinity, so this looks like an oversight rather than a deliberate choice.
Suggested fix
Enforce a finite cap during accumulation so it triggers before Buffer.concat. Minimal: default multipart's payloadLimit to a finite value (e.g. aligned with fileSizeLimit) so the existing totalSize > payloadLimit guard fires mid-stream. Happy to open a PR if that direction is acceptable.
Summary
fileSizeLimitdoes not bound memory for multipart bodies, because it is validated after the entire request body has already been buffered and concatenated. A single in-flightmultipart/form-datarequest under the default limits can pin hundreds of MB of server memory.Detail (at HEAD, v5.1.1)
The shared accumulator in
p()pushes every chunk into an in-memory array and only guards againstpayloadLimit(src/index.ts):multipart()explicitly setspayloadLimit = Number.POSITIVE_INFINITY, so that guard never fires. The only multipart cap,fileSizeLimit(default 200 MiB), is checked insideparseMultipart, afterBuffer.concat+td.decode(src/index.ts~line 159:if (data.length > fileSizeLimit) throw …). So it validates after the memory is already consumed — it is a post-hoc size check, not a memory guard.Measured with a plain
app.use(multipart())server (Node 24): a 150 MiB body — under both default limits — returns 200 while server RSS grows ~+460 MiB and stays there; N concurrent such requests OOM the process. (400/600 MiB bodies do hitfileSizeLimit/V8 string-length, but only after buffering.)Note: #22 added request limits to the other parsers but multipart kept
Infinity, so this looks like an oversight rather than a deliberate choice.Suggested fix
Enforce a finite cap during accumulation so it triggers before
Buffer.concat. Minimal: defaultmultipart'spayloadLimitto a finite value (e.g. aligned withfileSizeLimit) so the existingtotalSize > payloadLimitguard fires mid-stream. Happy to open a PR if that direction is acceptable.