Skip to content

fix: guard PersistentFile.write() against a destroyed write stream - #1121

Open
raphyabak wants to merge 1 commit into
node-formidable:masterfrom
raphyabak:fix/persistent-file-write-after-destroy
Open

fix: guard PersistentFile.write() against a destroyed write stream#1121
raphyabak wants to merge 1 commit into
node-formidable:masterfrom
raphyabak:fix/persistent-file-write-after-destroy

Conversation

@raphyabak

@raphyabak raphyabak commented Aug 28, 2026

Copy link
Copy Markdown

Summary

Fixes #958.

PersistentFile.write() only checked this._writeStream.closed before writing:

if (this._writeStream.closed) {
  cb();
  return;
}

VolatileFile.write() (the sibling class) already checks both:

if (this._writeStream.closed || this._writeStream.destroyed) {

When a request is aborted mid-upload, destroy() calls this._writeStream.destroy(), which flips .destroyed synchronously — but .closed only flips later, once the underlying fd actually finishes closing. A write() call landing in that window skips the (incomplete) guard and reaches the real fs write stream, which throws ERR_STREAM_DESTROYED ("Cannot call write after a stream was destroyed") outside of any callback formidable can catch, crashing the process. This matches the exact stack trace and crash pattern in the issue, and was correctly diagnosed there by @tommyhtran, comparing the two classes' write() methods.

Fix

Add the same .destroyed check VolatileFile already has.

Test plan

  • Added test/unit/persistent-file-write.test.js covering write() with a mock stream in three states: normal, closed, and destroyed-but-not-yet-closed (the state that reproduces the crash).
  • Verified the regression: reverting the source change turns the third test from passing into a fast, clear assertion failure (write was called when it shouldn't have been) — not a flaky timeout, since the mock stream's write always resolves its callback regardless of whether the guard should have short-circuited first.
  • Full suite passes: pnpm test-jest — 15 suites, 94 passing, 3 pre-existing skips (unrelated).
  • eslint passes on both changed files (matches this repo's existing lint baseline — the one remaining @jest/globals resolution warning already exists on the pre-existing volatile-file.test.js too, unrelated to this change).

Greptile Summary

This PR prevents PersistentFile.write() from writing to a destroyed stream during upload cleanup, matching the existing VolatileFile lifecycle guard.

  • Treats destroyed and closed persistent write streams as no-op write targets.
  • Adds unit coverage for active, closed, and destroyed-but-not-yet-closed stream states.

Confidence Score: 5/5

The PR appears safe to merge, with the destroyed-stream guard addressing the abort-time write failure without introducing an established regression.

The production change narrowly aligns persistent-file behavior with the sibling volatile-file implementation, and the tests exercise the relevant active, closed, and destroyed stream states.

Important Files Changed

Filename Overview
src/PersistentFile.js Extends the existing closed-stream guard to prevent writes after synchronous stream destruction.
test/unit/persistent-file-write.test.js Adds focused regression tests confirming writes proceed only while the underlying stream remains active.

Reviews (1): Last reviewed commit: "fix: guard PersistentFile.write() agains..." | Re-trigger Greptile

Context used:

Fixes node-formidable#958.

PersistentFile.write() only checked this._writeStream.closed before
writing, unlike VolatileFile.write() which already checks both
.closed and .destroyed. When a request is aborted mid-upload,
destroy() calls this._writeStream.destroy(), which flips .destroyed
synchronously but only flips .closed later, once the underlying fd
finishes closing. A write() call landing in that window bypassed the
guard and reached the real fs write stream, which throws
ERR_STREAM_DESTROYED ("Cannot call write after a stream was
destroyed") outside of any callback formidable can catch, crashing
the process.

Add the same .destroyed check PersistentFile's sibling class already
has.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression in Formidable v3, which can crash a server

1 participant