From cd4f4d2932167feda0f9fc3fa5bd33727ea0f9b1 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Wed, 1 Jul 2026 22:57:48 +0100 Subject: [PATCH 1/4] fix: enforce maxFileSize and maxTotalFileSize on octet-stream uploads The octet-stream upload path wrote every chunk to disk without checking the documented maxFileSize/maxTotalFileSize limits, unlike the multipart path in _handlePart. Accumulate per-file and total sizes and abort via _error with the existing FormidableError codes when a cap is exceeded, mirroring the multipart implementation. The over-limit file is removed through the shared _error cleanup, so no partial bytes remain on disk. --- src/plugins/octetstream.js | 27 ++++++++++++++++++++++ test/integration/octet-stream.test.js | 32 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/plugins/octetstream.js b/src/plugins/octetstream.js index 8a0b2709..2fed2eb5 100644 --- a/src/plugins/octetstream.js +++ b/src/plugins/octetstream.js @@ -1,6 +1,8 @@ /* eslint-disable no-underscore-dangle */ import OctetStreamParser from "../parsers/OctetStream.js"; +import * as errors from "../FormidableError.js"; +import FormidableError from "../FormidableError.js"; export const octetStreamType = "octet-stream"; // the `options` is also available through the `options` / `formidable.options` @@ -47,8 +49,33 @@ async function init(_self, _opts) { // Keep track of writes that haven't finished so we don't emit the file before it's done being written let outstandingWrites = 0; + let fileSize = 0; this._parser.on("data", (buffer) => { + fileSize += buffer.length; + this._totalFileSize += buffer.length; + + if (fileSize > this.options.maxFileSize) { + this._error( + new FormidableError( + `options.maxFileSize (${this.options.maxFileSize} bytes), received ${fileSize} bytes of file data`, + errors.biggerThanMaxFileSize, + 413 + ) + ); + return; + } + if (this._totalFileSize > this.options.maxTotalFileSize) { + this._error( + new FormidableError( + `options.maxTotalFileSize (${this.options.maxTotalFileSize} bytes) exceeded, received ${this._totalFileSize} bytes of file data`, + errors.biggerThanTotalMaxFileSize, + 413 + ) + ); + return; + } + this.pause(); outstandingWrites += 1; diff --git a/test/integration/octet-stream.test.js b/test/integration/octet-stream.test.js index b6b6ab11..bc30743e 100644 --- a/test/integration/octet-stream.test.js +++ b/test/integration/octet-stream.test.js @@ -52,3 +52,35 @@ test("octet stream", (done) => { createReadStream(testFilePath).pipe(request); }); }); + +test("octet stream enforces maxFileSize", (done) => { + const PORT2 = PORT + 1; + const server = createServer((req, res) => { + const form = formidable({ maxFileSize: 1024, maxTotalFileSize: 2048 }); + + form.parse(req, (err, fields, files) => { + // a 256KB octet-stream body must be rejected, not written to disk + assert(err, "expected an error for over-sized octet-stream upload"); + strictEqual(err.code, 1016); // biggerThanMaxFileSize + strictEqual(Object.keys(files).length, 0); + + res.end(); + server.close(); + done(); + }); + }); + + server.listen(PORT2, (err) => { + assert(!err, "should not have error, but be falsey"); + + const request = _request({ + port: PORT2, + method: "POST", + headers: { + "Content-Type": "application/octet-stream", + }, + }); + + request.end(Buffer.alloc(256 * 1024, 0x42)); + }); +}); From 1a43a6198488331ec9661a8ab03d9ad1a5ef82ce Mon Sep 17 00:00:00 2001 From: Yarchik Date: Fri, 25 Sep 2026 16:32:38 +0100 Subject: [PATCH 2/4] test(octet-stream): close the rejected upload's connection The maxFileSize test left a TCPWRAP handle open: formidable aborts the parse mid-body, so the connection never completes on its own and server.close() only stops new ones. Jest reported "did not exit one second after the test run" locally and "A worker process has failed to exit gracefully" on CI, where the leaked socket's late callback surfaced as a failure inside the next test file, test/integration/store-files-option.test.js. Destroying the client request ends the connection. Measured: with the old teardown --detectOpenHandles reports 1 open TCPWRAP at the server.listen line; with this change it reports none, and the full suite is 15/15 suites, 95 passed / 3 skipped of 98, with no worker warning. --- test/integration/octet-stream.test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/integration/octet-stream.test.js b/test/integration/octet-stream.test.js index bc30743e..61f56540 100644 --- a/test/integration/octet-stream.test.js +++ b/test/integration/octet-stream.test.js @@ -55,6 +55,7 @@ test("octet stream", (done) => { test("octet stream enforces maxFileSize", (done) => { const PORT2 = PORT + 1; + let request; const server = createServer((req, res) => { const form = formidable({ maxFileSize: 1024, maxTotalFileSize: 2048 }); @@ -65,6 +66,9 @@ test("octet stream enforces maxFileSize", (done) => { strictEqual(Object.keys(files).length, 0); res.end(); + // The upload was rejected mid-body, so the connection never completes on + // its own; without this the socket outlives the test and Jest's worker. + request.destroy(); server.close(); done(); }); @@ -73,7 +77,7 @@ test("octet stream enforces maxFileSize", (done) => { server.listen(PORT2, (err) => { assert(!err, "should not have error, but be falsey"); - const request = _request({ + request = _request({ port: PORT2, method: "POST", headers: { @@ -81,6 +85,8 @@ test("octet stream enforces maxFileSize", (done) => { }, }); + // Destroying the request above surfaces here as ECONNRESET. + request.on("error", () => {}); request.end(Buffer.alloc(256 * 1024, 0x42)); }); }); From 392b42b2b2b4f44cfdecd876b926b0ded57bb8f7 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Tue, 20 Oct 2026 23:08:08 +0200 Subject: [PATCH 3/4] refactor: combine the two FormidableError imports --- src/plugins/octetstream.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plugins/octetstream.js b/src/plugins/octetstream.js index 2fed2eb5..c72166de 100644 --- a/src/plugins/octetstream.js +++ b/src/plugins/octetstream.js @@ -1,8 +1,7 @@ /* eslint-disable no-underscore-dangle */ import OctetStreamParser from "../parsers/OctetStream.js"; -import * as errors from "../FormidableError.js"; -import FormidableError from "../FormidableError.js"; +import FormidableError, * as errors from "../FormidableError.js"; export const octetStreamType = "octet-stream"; // the `options` is also available through the `options` / `formidable.options` From f5e06b0197f9346265f7fd19bfd2e1f04cfb1873 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Tue, 20 Oct 2026 23:08:08 +0200 Subject: [PATCH 4/4] test: fail on a request error raised before the upload is rejected Use the named biggerThanMaxFileSize code and drop the explanatory comments. --- test/integration/octet-stream.test.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/integration/octet-stream.test.js b/test/integration/octet-stream.test.js index 61f56540..d1551b96 100644 --- a/test/integration/octet-stream.test.js +++ b/test/integration/octet-stream.test.js @@ -5,6 +5,7 @@ import url from "node:url"; import assert, { strictEqual, deepStrictEqual } from "node:assert"; import formidable from "../../src/index.js"; +import { biggerThanMaxFileSize } from "../../src/FormidableError.js"; const __filename = url.fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -56,18 +57,17 @@ test("octet stream", (done) => { test("octet stream enforces maxFileSize", (done) => { const PORT2 = PORT + 1; let request; + let parsed = false; const server = createServer((req, res) => { const form = formidable({ maxFileSize: 1024, maxTotalFileSize: 2048 }); form.parse(req, (err, fields, files) => { - // a 256KB octet-stream body must be rejected, not written to disk assert(err, "expected an error for over-sized octet-stream upload"); - strictEqual(err.code, 1016); // biggerThanMaxFileSize + strictEqual(err.code, biggerThanMaxFileSize); strictEqual(Object.keys(files).length, 0); res.end(); - // The upload was rejected mid-body, so the connection never completes on - // its own; without this the socket outlives the test and Jest's worker. + parsed = true; request.destroy(); server.close(); done(); @@ -85,8 +85,11 @@ test("octet stream enforces maxFileSize", (done) => { }, }); - // Destroying the request above surfaces here as ECONNRESET. - request.on("error", () => {}); + request.on("error", (error) => { + if (parsed) return; + server.close(); + done(error); + }); request.end(Buffer.alloc(256 * 1024, 0x42)); }); });