Skip to content

fix: EBADF when calling fileHandle.close() after streaming via pipeline#1249

Merged
streamich merged 2 commits intomasterfrom
copilot/fix-streaming-breaks-file-handles
Mar 21, 2026
Merged

fix: EBADF when calling fileHandle.close() after streaming via pipeline#1249
streamich merged 2 commits intomasterfrom
copilot/fix-streaming-breaks-file-handles

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 21, 2026

fileHandle.close() throws EBADF after consuming a ReadStream created from that FileHandle, because the stream's autoClose path called vol.close(numericFd) directly — bypassing the FileHandle's ref-counted lifecycle. The FileHandle still held its fd, so its own close() attempted to close an already-closed descriptor.

Changes

  • FsReadStream / FsWriteStream — store the originating FileHandle as this._fileHandle when options.fd is a FileHandle object. In close(), route through fileHandle.close() instead of vol.close(numericFd). This marks the FileHandle's internal fd as -1, so any subsequent explicit fileHandle.close() call returns Promise.resolve() immediately without error.

  • IFileHandle interface + FileHandle implementation — made createReadStream / createWriteStream options parameters optional (?), consistent with Node.js's own API and the already-optional readableWebStream options.

Example

const fileHandle = await fsp.open(p, fs.constants.O_RDONLY);
const s = fileHandle.createReadStream();
await pipeline(s, new stream.Writable({ write: (c, e, cb) => cb() }));
await fileHandle.close(); // previously threw EBADF — now resolves cleanly
Original prompt

This section details on the original issue you should resolve

<issue_title>streaming breaks file handles</issue_title>
<issue_description>We get EBADF after streaming from a file handle:

import test from 'node:test';
import assert from 'node:assert';
import path from 'node:path';
import * as stream from 'node:stream';

import * as memfsModule from 'memfs';
import * as fs from 'fs/promises';
import { pipeline } from 'node:stream/promises';

const memfs = memfsModule.fs.promises;

await test(
  'fs allows close after streaming', () => testAutoClose(path.resolve(), fs)
);

await test(
  'memfs should allow close after streaming', () => testAutoClose('/', memfs as unknown as typeof fs)
);

async function testAutoClose(root: string, fsp: typeof fs) {
  const p = path.resolve(root, 'testFile');
  await fsp.writeFile(p, 'teststring');
  try {
    const fileHandle = await fsp.open(p, fs.constants.O_RDONLY);
    const s = fileHandle.createReadStream();
    await pipeline(s, new stream.Writable({ write: (c, e, cb) => { cb(); } }))
    await assert.doesNotReject(fileHandle.close());
  } finally {
    await fsp.rm(p);
  }
}
dolan@MacBookPro ops % node test/memfs.test.mts
✔ fs allows close after streaming (3.209875ms)
✖ memfs should allow close after streaming (2.080458ms)
ℹ tests 2
ℹ suites 0
ℹ pass 1
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 7.617667

✖ failing tests:

test at test/memfs.test.mts:35:7
✖ memfs should allow close after streaming (2.080458ms)
  AssertionError [ERR_ASSERTION]: Got unwanted rejection.
  Actual message: "EBADF: bad file descriptor, close"
      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
      at async testAutoClose (file:///Users/dolan/IdeaProjects/papertrace/code/ops/test/memfs.test.mts:46:5)
      at async Test.run (node:internal/test_runner/test:1113:7)
      at async startSubtestAfterBootstrap (node:internal/test_runner/harness:358:3)
      at async file:///Users/dolan/IdeaProjects/papertrace/code/ops/test/memfs.test.mts:35:1 {
    generatedMessage: false,
    code: 'ERR_ASSERTION',
    actual: [Error],
    expected: undefined,
    operator: 'doesNotReject',
    diff: 'simple'
  }

Node 24.11.0
memfs 4.56.11
Mac OS Sequoia 15.7.3 </issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Fix EBADF error after streaming from file handle fix: EBADF when calling fileHandle.close() after streaming via pipeline Mar 21, 2026
Copilot AI requested a review from streamich March 21, 2026 17:12
@streamich streamich marked this pull request as ready for review March 21, 2026 17:13
Copilot AI review requested due to automatic review settings March 21, 2026 17:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a EBADF double-close scenario when a ReadStream/WriteStream is created from a FileHandle and consumed via pipeline(), ensuring the stream’s auto-close path updates the FileHandle lifecycle state correctly.

Changes:

  • Update FsReadStream / FsWriteStream to retain the originating FileHandle (when provided via options.fd) and close via fileHandle.close() rather than vol.close(fd).
  • Make IFileHandle.createReadStream / createWriteStream options parameters optional to align with Node.js semantics.
  • Add regression tests covering pipeline() + subsequent explicit fileHandle.close() for both read and write streams.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
packages/fs-node/src/volume.ts Routes stream closure through FileHandle.close() when constructed from a FileHandle, preventing descriptor state mismatch.
packages/fs-node/src/FileHandle.ts Aligns createReadStream/createWriteStream signatures with optional options.
packages/fs-node-utils/src/types/misc.ts Updates IFileHandle interface so stream creation options are optional.
packages/fs-node/src/tests/volume/FileHandle.test.ts Adds regression tests verifying handle.close() resolves after streaming via pipeline().

@streamich streamich merged commit e712f63 into master Mar 21, 2026
13 checks passed
@streamich streamich deleted the copilot/fix-streaming-breaks-file-handles branch March 21, 2026 17:19
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.

streaming breaks file handles

3 participants