Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions lib/internal/readline/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,9 @@ class Interface extends InterfaceConstructor {
}
this.closed = true;
this.emit('close');
const reject = this[kQuestionReject];
this[kQuestionReject] = null;
reject?.(new AbortError());
}

/**
Expand Down Expand Up @@ -1327,8 +1330,10 @@ class Interface extends InterfaceConstructor {
this.emit('SIGINT');
} else {
// This readline instance is finished
const rejectC = this[kQuestionReject];
this[kQuestionReject] = null;
this.close();
this[kQuestionReject]?.(new AbortError('Aborted with Ctrl+C'));
rejectC?.(new AbortError('Aborted with Ctrl+C'));
}
break;

Expand All @@ -1339,8 +1344,10 @@ class Interface extends InterfaceConstructor {
case 'd': // delete right or EOF
if (this.cursor === 0 && this.line.length === 0) {
// This readline instance is finished
const rejectD = this[kQuestionReject];
this[kQuestionReject] = null;
this.close();
this[kQuestionReject]?.(new AbortError('Aborted with Ctrl+D'));
rejectD?.(new AbortError('Aborted with Ctrl+D'));
} else if (this.cursor < this.line.length) {
this[kDeleteRight]();
}
Expand Down
6 changes: 4 additions & 2 deletions test/parallel/test-readline-promises-interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,8 @@ function assertCursorRowsAndCols(rli, rows, cols) {
{
const [rli] = getInterface({ terminal: true });
const expectedLines = ['foo'];
rli.question(expectedLines[0]).then(() => rli.close()).then(common.mustNotCall('never settling promise'));
// close() should reject a pending question with AbortError
assert.rejects(rli.question(expectedLines[0]), { name: 'AbortError' }).then(common.mustCall());
assertCursorRowsAndCols(rli, 0, expectedLines[0].length);
rli.close();
}
Expand All @@ -405,7 +406,8 @@ function assertCursorRowsAndCols(rli, rows, cols) {
{
const [rli] = getInterface({ terminal: true });
const expectedLines = ['foo', 'bar'];
rli.question(expectedLines.join('\n')).then(() => rli.close()).then(common.mustNotCall('never settling promise'));
// close() should reject a pending question with AbortError
assert.rejects(rli.question(expectedLines.join('\n')), { name: 'AbortError' }).then(common.mustCall());
assertCursorRowsAndCols(
rli, expectedLines.length - 1, expectedLines.slice(-1)[0].length);
rli.close();
Expand Down
64 changes: 64 additions & 0 deletions test/parallel/test-readline-promises-question-close.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict';

// Tests that calling rl.close() while a question() promise is pending rejects
// the promise with AbortError instead of leaving it permanently unresolved.

const common = require('../common');
const assert = require('node:assert');
const readline = require('node:readline/promises');
const { Readable } = require('node:stream');
const { EventEmitter } = require('node:events');
const { Writable } = require('node:stream');

class FakeInput extends EventEmitter {
resume() {}
pause() {}
write() {}
end() {}
}

// Programmatic close() rejects the pending question
{
const input = new Readable({ read() {} });
const rl = readline.createInterface({ input, terminal: false });
assert.rejects(rl.question('prompt: '), { name: 'AbortError' }).then(common.mustCall());
rl.close();
}

// close() rejects only once when called multiple times
{
const input = new Readable({ read() {} });
const rl = readline.createInterface({ input, terminal: false });
assert.rejects(rl.question('prompt: '), { name: 'AbortError' }).then(common.mustCall());
rl.close();
rl.close(); // Second close() should be a no-op
}

// close() does not throw if there is no pending question
{
const input = new Readable({ read() {} });
const rl = readline.createInterface({ input, terminal: false });
rl.close(); // Should not throw
}

// Terminal interface: close() rejects a pending question
{
const fi = new FakeInput();
const output = new Writable({ write(chunk, enc, cb) { cb(); } });
const rl = readline.createInterface({ terminal: true, input: fi, output });
assert.rejects(rl.question('prompt: '), { name: 'AbortError' }).then(common.mustCall());
rl.close();
}

// Answering before close() resolves normally; subsequent close() is no-op
{
const fi = new FakeInput();
const output = new Writable({ write(chunk, enc, cb) { cb(); } });
const rl = readline.createInterface({ terminal: true, input: fi, output });
rl.question('prompt: ').then(common.mustCall((answer) => {
assert.strictEqual(answer, 'hello');
}));
fi.emit('data', 'hello\n');
// close() with no pending question should not throw
setImmediate(common.mustCall(() => rl.close()));
}
Loading