http: fix double AbortSignal registration

Fix an issue where AbortSignals are registered twice

PR-URL: https://github.com/nodejs/node/pull/37730
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
This commit is contained in:
Nitzan Uziely 2021-03-12 20:24:00 +02:00 committed by Rich Trott
parent a41c3e17ad
commit 711e210d35
5 changed files with 143 additions and 17 deletions

View File

@ -288,14 +288,20 @@ function ClientRequest(input, options, cb) {
options.headers);
}
let optsWithoutSignal = options;
if (optsWithoutSignal.signal) {
optsWithoutSignal = ObjectAssign({}, options);
delete optsWithoutSignal.signal;
}
// initiate connection
if (this.agent) {
this.agent.addRequest(this, options);
this.agent.addRequest(this, optsWithoutSignal);
} else {
// No agent, default to Connection:close.
this._last = true;
this.shouldKeepAlive = false;
if (typeof options.createConnection === 'function') {
if (typeof optsWithoutSignal.createConnection === 'function') {
const oncreate = once((err, socket) => {
if (err) {
process.nextTick(() => emitError(this, err));
@ -305,7 +311,8 @@ function ClientRequest(input, options, cb) {
});
try {
const newSocket = options.createConnection(options, oncreate);
const newSocket = optsWithoutSignal.createConnection(optsWithoutSignal,
oncreate);
if (newSocket) {
oncreate(null, newSocket);
}
@ -313,8 +320,8 @@ function ClientRequest(input, options, cb) {
oncreate(err);
}
} else {
debug('CLIENT use net.createConnection', options);
this.onSocket(net.createConnection(options));
debug('CLIENT use net.createConnection', optsWithoutSignal);
this.onSocket(net.createConnection(optsWithoutSignal));
}
}
}

View File

@ -0,0 +1,73 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const Agent = http.Agent;
const { getEventListeners, once } = require('events');
const agent = new Agent();
const server = http.createServer();
server.listen(0, common.mustCall(async () => {
const port = server.address().port;
const host = 'localhost';
const options = {
port: port,
host: host,
_agentKey: agent.getName({ port, host })
};
async function postCreateConnection() {
const ac = new AbortController();
const { signal } = ac;
const connection = agent.createConnection({ ...options, signal });
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
ac.abort();
const [err] = await once(connection, 'error');
assert.strictEqual(err?.name, 'AbortError');
}
async function preCreateConnection() {
const ac = new AbortController();
const { signal } = ac;
ac.abort();
const connection = agent.createConnection({ ...options, signal });
const [err] = await once(connection, 'error');
assert.strictEqual(err?.name, 'AbortError');
}
async function agentAsParam() {
const ac = new AbortController();
const { signal } = ac;
const request = http.get({
port: server.address().port,
path: '/hello',
agent: agent,
signal,
});
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
ac.abort();
const [err] = await once(request, 'error');
assert.strictEqual(err?.name, 'AbortError');
}
async function agentAsParamPreAbort() {
const ac = new AbortController();
const { signal } = ac;
ac.abort();
const request = http.get({
port: server.address().port,
path: '/hello',
agent: agent,
signal,
});
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
const [err] = await once(request, 'error');
assert.strictEqual(err?.name, 'AbortError');
}
await postCreateConnection();
await preCreateConnection();
await agentAsParam();
await agentAsParamPreAbort();
server.close();
}));

View File

@ -2,6 +2,7 @@
const common = require('../common');
const http = require('http');
const assert = require('assert');
const { getEventListeners } = require('events');
{
// abort
@ -71,19 +72,20 @@ const assert = require('assert');
{
// Destroy with AbortSignal
// Destroy post-abort sync with AbortSignal
const server = http.createServer(common.mustNotCall());
const controller = new AbortController();
const { signal } = controller;
server.listen(0, common.mustCall(() => {
const options = { port: server.address().port, signal: controller.signal };
const options = { port: server.address().port, signal };
const req = http.get(options, common.mustNotCall());
req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ABORT_ERR');
assert.strictEqual(err.name, 'AbortError');
server.close();
}));
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
controller.abort();
@ -91,3 +93,47 @@ const assert = require('assert');
assert.strictEqual(req.destroyed, true);
}));
}
{
// Use post-abort async AbortSignal
const server = http.createServer(common.mustNotCall());
const controller = new AbortController();
const { signal } = controller;
server.listen(0, common.mustCall(() => {
const options = { port: server.address().port, signal };
const req = http.get(options, common.mustNotCall());
req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ABORT_ERR');
assert.strictEqual(err.name, 'AbortError');
}));
req.on('close', common.mustCall(() => {
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
server.close();
}));
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
process.nextTick(() => controller.abort());
}));
}
{
// Use pre-aborted AbortSignal
const server = http.createServer(common.mustNotCall());
const controller = new AbortController();
const { signal } = controller;
server.listen(0, common.mustCall(() => {
controller.abort();
const options = { port: server.address().port, signal };
const req = http.get(options, common.mustNotCall());
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ABORT_ERR');
assert.strictEqual(err.name, 'AbortError');
server.close();
}));
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
}));
}

View File

@ -8,9 +8,8 @@ if (!common.hasCrypto)
const assert = require('assert');
const h2 = require('http2');
const { kSocket } = require('internal/http2/util');
const { kEvents } = require('internal/event_target');
const Countdown = require('../common/countdown');
const { getEventListeners } = require('events');
{
const server = h2.createServer();
server.listen(0, common.mustCall(() => {
@ -180,11 +179,11 @@ const Countdown = require('../common/countdown');
client.on('close', common.mustCall());
const { signal } = controller;
assert.strictEqual(signal[kEvents].get('abort'), undefined);
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
client.on('error', common.mustCall(() => {
// After underlying stream dies, signal listener detached
assert.strictEqual(signal[kEvents].get('abort'), undefined);
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
}));
const req = client.request({}, { signal });
@ -198,7 +197,7 @@ const Countdown = require('../common/countdown');
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
// Signal listener attached
assert.strictEqual(signal[kEvents].get('abort').size, 1);
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
controller.abort();
@ -219,16 +218,16 @@ const Countdown = require('../common/countdown');
const { signal } = controller;
controller.abort();
assert.strictEqual(signal[kEvents].get('abort'), undefined);
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
client.on('error', common.mustCall(() => {
// After underlying stream dies, signal listener detached
assert.strictEqual(signal[kEvents].get('abort'), undefined);
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
}));
const req = client.request({}, { signal });
// Signal already aborted, so no event listener attached.
assert.strictEqual(signal[kEvents].get('abort'), undefined);
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
assert.strictEqual(req.aborted, false);
// Destroyed on same tick as request made

View File

@ -7,7 +7,7 @@ if (!common.hasCrypto)
const fixtures = require('../common/fixtures');
const https = require('https');
const assert = require('assert');
const { once } = require('events');
const { once, getEventListeners } = require('events');
const options = {
key: fixtures.readKey('agent1-key.pem'),
@ -29,6 +29,7 @@ const options = {
rejectUnauthorized: false,
signal: ac.signal,
});
assert.strictEqual(getEventListeners(ac.signal, 'abort').length, 1);
process.nextTick(() => ac.abort());
const [ err ] = await once(req, 'error');
assert.strictEqual(err.name, 'AbortError');