Skip to content

Commit 4a99cc2

Browse files
committed
fs: prevent multiple invocations of callback in Dir class
1 parent 264cad7 commit 4a99cc2

File tree

2 files changed

+63
-10
lines changed

2 files changed

+63
-10
lines changed

lib/internal/fs/dir.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,22 +115,22 @@ class Dir {
115115
return;
116116
}
117117

118+
let dirent;
118119
if (this.#processHandlerQueue()) {
119120
try {
120-
const dirent = ArrayPrototypeShift(this.#bufferedEntries);
121+
dirent = ArrayPrototypeShift(this.#bufferedEntries);
121122

122123
if (this.#options.recursive && dirent.isDirectory()) {
123124
this.readSyncRecursive(dirent);
124125
}
125-
126-
if (maybeSync)
127-
process.nextTick(callback, null, dirent);
128-
else
129-
callback(null, dirent);
130-
return;
131126
} catch (error) {
132127
return callback(error);
133128
}
129+
if (maybeSync)
130+
process.nextTick(callback, null, dirent);
131+
else
132+
callback(null, dirent);
133+
return;
134134
}
135135

136136
const req = new FSReqCallback();
@@ -145,16 +145,17 @@ class Dir {
145145
return callback(err, result);
146146
}
147147

148+
let dirent;
148149
try {
149150
this.processReadResult(this.#path, result);
150-
const dirent = ArrayPrototypeShift(this.#bufferedEntries);
151+
dirent = ArrayPrototypeShift(this.#bufferedEntries);
151152
if (this.#options.recursive && dirent.isDirectory()) {
152153
this.readSyncRecursive(dirent);
153154
}
154-
callback(null, dirent);
155155
} catch (error) {
156-
callback(error);
156+
return callback(error);
157157
}
158+
return callback(null, dirent);
158159
};
159160

160161
this.#operationQueue = [];

test/parallel/test-fs-opendir.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const common = require('../common');
44
const assert = require('assert');
55
const fs = require('fs');
66
const path = require('path');
7+
const test = require('node:test');
78

89
const tmpdir = require('../common/tmpdir');
910

@@ -287,3 +288,54 @@ doConcurrentAsyncMixedOps().then(common.mustCall());
287288
dir.closeSync();
288289
assert.rejects(dir.close(), dirclosedError).then(common.mustCall());
289290
}
291+
292+
test('fs.opendir should not double callback if user callback throws', async (t) => {
293+
let readCallbackInvokedCount = 0;
294+
let userErrorThrown = false;
295+
296+
const dir = await fs.promises.opendir('.');
297+
298+
t.after(() => dir.close().catch((err) => console.error('Error closing dir in t.after:', err)));
299+
300+
await new Promise((resolve, reject) => {
301+
const operationTimeout = setTimeout(() => {
302+
if (userErrorThrown && readCallbackInvokedCount === 1) {
303+
resolve(); // User error handled, no double callback
304+
} else if (readCallbackInvokedCount === 0) {
305+
reject(new Error('dir.read callback was never invoked.'));
306+
} else {
307+
reject(new Error('Test timeout reached in an unexpected state.'));
308+
}
309+
}, 1000);
310+
311+
dir.read((err, dirent) => {
312+
readCallbackInvokedCount++;
313+
314+
if (err) {
315+
// This is an error from dir.read() itself, not the user error we're testing.
316+
clearTimeout(operationTimeout);
317+
return reject(new Error(`dir.read reported an error: ${err.message}`));
318+
}
319+
320+
if (readCallbackInvokedCount === 1) {
321+
userErrorThrown = true;
322+
// Simulate the user's code throwing an error.
323+
// A robust fs.Dir should catch this internally and not call this callback again.
324+
throw new Error('Simulated user error in dir.read callback');
325+
}
326+
327+
if (readCallbackInvokedCount > 1) {
328+
clearTimeout(operationTimeout);
329+
return reject(new Error('dir.read callback was invoked multiple times.'));
330+
}
331+
332+
// If dirent is null and it's the first call, and no user error was meant to be thrown yet,
333+
// it implies an empty directory or an issue with the test setup if we expected more entries.
334+
// For this specific test, we expect the user error to be thrown on the first entry.
335+
if (dirent === null && readCallbackInvokedCount === 1 && !userErrorThrown) {
336+
clearTimeout(operationTimeout);
337+
return reject(new Error('Directory reading finished before user error could be simulated.'));
338+
}
339+
});
340+
});
341+
});

0 commit comments

Comments
 (0)