Skip to content

Commit daad3fd

Browse files
committed
lib: reject pending question promise on close
1 parent 6009d93 commit daad3fd

3 files changed

Lines changed: 77 additions & 4 deletions

File tree

lib/internal/readline/interface.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,9 @@ class Interface extends InterfaceConstructor {
553553
}
554554
this.closed = true;
555555
this.emit('close');
556+
const reject = this[kQuestionReject];
557+
this[kQuestionReject] = null;
558+
reject?.(new AbortError());
556559
}
557560

558561
/**
@@ -1327,8 +1330,10 @@ class Interface extends InterfaceConstructor {
13271330
this.emit('SIGINT');
13281331
} else {
13291332
// This readline instance is finished
1333+
const rejectC = this[kQuestionReject];
1334+
this[kQuestionReject] = null;
13301335
this.close();
1331-
this[kQuestionReject]?.(new AbortError('Aborted with Ctrl+C'));
1336+
rejectC?.(new AbortError('Aborted with Ctrl+C'));
13321337
}
13331338
break;
13341339

@@ -1339,8 +1344,10 @@ class Interface extends InterfaceConstructor {
13391344
case 'd': // delete right or EOF
13401345
if (this.cursor === 0 && this.line.length === 0) {
13411346
// This readline instance is finished
1347+
const rejectD = this[kQuestionReject];
1348+
this[kQuestionReject] = null;
13421349
this.close();
1343-
this[kQuestionReject]?.(new AbortError('Aborted with Ctrl+D'));
1350+
rejectD?.(new AbortError('Aborted with Ctrl+D'));
13441351
} else if (this.cursor < this.line.length) {
13451352
this[kDeleteRight]();
13461353
}

test/parallel/test-readline-promises-interface.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,8 @@ function assertCursorRowsAndCols(rli, rows, cols) {
396396
{
397397
const [rli] = getInterface({ terminal: true });
398398
const expectedLines = ['foo'];
399-
rli.question(expectedLines[0]).then(() => rli.close()).then(common.mustNotCall('never settling promise'));
399+
// close() should reject a pending question with AbortError
400+
assert.rejects(rli.question(expectedLines[0]), { name: 'AbortError' }).then(common.mustCall());
400401
assertCursorRowsAndCols(rli, 0, expectedLines[0].length);
401402
rli.close();
402403
}
@@ -405,7 +406,8 @@ function assertCursorRowsAndCols(rli, rows, cols) {
405406
{
406407
const [rli] = getInterface({ terminal: true });
407408
const expectedLines = ['foo', 'bar'];
408-
rli.question(expectedLines.join('\n')).then(() => rli.close()).then(common.mustNotCall('never settling promise'));
409+
// close() should reject a pending question with AbortError
410+
assert.rejects(rli.question(expectedLines.join('\n')), { name: 'AbortError' }).then(common.mustCall());
409411
assertCursorRowsAndCols(
410412
rli, expectedLines.length - 1, expectedLines.slice(-1)[0].length);
411413
rli.close();
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict';
2+
3+
// Tests that calling rl.close() while a question() promise is pending rejects
4+
// the promise with AbortError instead of leaving it permanently unresolved.
5+
6+
const common = require('../common');
7+
const assert = require('node:assert');
8+
const readline = require('node:readline/promises');
9+
const { Readable } = require('node:stream');
10+
const { EventEmitter } = require('node:events');
11+
const { Writable } = require('node:stream');
12+
13+
class FakeInput extends EventEmitter {
14+
resume() {}
15+
pause() {}
16+
write() {}
17+
end() {}
18+
}
19+
20+
// Programmatic close() rejects the pending question
21+
{
22+
const input = new Readable({ read() {} });
23+
const rl = readline.createInterface({ input, terminal: false });
24+
assert.rejects(rl.question('prompt: '), { name: 'AbortError' }).then(common.mustCall());
25+
rl.close();
26+
}
27+
28+
// close() rejects only once when called multiple times
29+
{
30+
const input = new Readable({ read() {} });
31+
const rl = readline.createInterface({ input, terminal: false });
32+
assert.rejects(rl.question('prompt: '), { name: 'AbortError' }).then(common.mustCall());
33+
rl.close();
34+
rl.close(); // second close() should be a no-op
35+
}
36+
37+
// close() does not throw if there is no pending question
38+
{
39+
const input = new Readable({ read() {} });
40+
const rl = readline.createInterface({ input, terminal: false });
41+
assert.doesNotThrow(() => rl.close());
42+
}
43+
44+
// Terminal interface: close() rejects a pending question
45+
{
46+
const fi = new FakeInput();
47+
const output = new Writable({ write(chunk, enc, cb) { cb(); } });
48+
const rl = readline.createInterface({ terminal: true, input: fi, output });
49+
assert.rejects(rl.question('prompt: '), { name: 'AbortError' }).then(common.mustCall());
50+
rl.close();
51+
}
52+
53+
// Answering before close() resolves normally; subsequent close() is no-op
54+
{
55+
const fi = new FakeInput();
56+
const output = new Writable({ write(chunk, enc, cb) { cb(); } });
57+
const rl = readline.createInterface({ terminal: true, input: fi, output });
58+
rl.question('prompt: ').then(common.mustCall((answer) => {
59+
assert.strictEqual(answer, 'hello');
60+
}));
61+
fi.emit('data', 'hello\n');
62+
// close() with no pending question should not throw
63+
setImmediate(common.mustCall(() => rl.close()));
64+
}

0 commit comments

Comments
 (0)