Skip to content

Commit

Permalink
http: fix double AbortSignal registration
Browse files Browse the repository at this point in the history
Fix an issue where AbortSignals are registered twice
  • Loading branch information
Linkgoron committed Mar 12, 2021
1 parent b9fd4eb commit fca3195
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 16 deletions.
17 changes: 12 additions & 5 deletions lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -305,16 +311,17 @@ function ClientRequest(input, options, cb) {
});

try {
const newSocket = options.createConnection(options, oncreate);
const newSocket = optsWithoutSignal.createConnection(optsWithoutSignal,
oncreate);
if (newSocket) {
oncreate(null, newSocket);
}
} catch (err) {
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));
}
}
}
Expand Down
94 changes: 94 additions & 0 deletions test/parallel/test-http-agent-abort-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const Agent = require('_http_agent').Agent;
const { getEventListeners } = require('events');
const agent = new Agent();
const server = http.createServer();

server.listen(0, common.mustCall(() => {
const port = server.address().port;
const host = 'localhost';
const options = {
port: port,
host: host,
rejectUnauthorized: false,
_agentKey: agent.getName({ port, host })
};

function postCreateConnection() {
return new Promise((res) => {
const ac = new AbortController();
const { signal } = ac;
const connection = agent.createConnection({ ...options, signal });
connection.on('error', common.mustCall((err) => {
assert(err);
assert.strictEqual(err.name, 'AbortError');
res();
}));
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
ac.abort();
});
}

function preCreateConnection() {
return new Promise((res) => {
const ac = new AbortController();
const { signal } = ac;
ac.abort();
const connection = agent.createConnection({ ...options, signal });
connection.on('error', common.mustCall((err) => {
assert(err);
assert.strictEqual(err.name, 'AbortError');
res();
}));
});
}

function agentAsParam() {
return new Promise((res) => {
const ac = new AbortController();
const { signal } = ac;
const request = http.get({
port: server.address().port,
path: '/hello',
agent: agent,
signal,
});
request.on('error', common.mustCall((err) => {
assert(err);
assert.strictEqual(err.name, 'AbortError');
res();
}));
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
ac.abort();
});
}

function agentAsParamPreAbort() {
return new Promise((res) => {
const ac = new AbortController();
const { signal } = ac;
ac.abort();
const request = http.get({
port: server.address().port,
path: '/hello',
agent: agent,
signal,
});
request.on('error', common.mustCall((err) => {
assert(err);
assert.strictEqual(err.name, 'AbortError');
res();
}));
assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
});
}

postCreateConnection()
.then(common.mustCall(preCreateConnection))
.then(common.mustCall(agentAsParam))
.then(common.mustCall(agentAsParamPreAbort))
.then(common.mustCall(() => server.close()));
}));
25 changes: 23 additions & 2 deletions test/parallel/test-http-client-abort-destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const common = require('../common');
const http = require('http');
const assert = require('assert');
const { getEventListeners } = require('events');

{
// abort
Expand Down Expand Up @@ -75,19 +76,39 @@ const assert = require('assert');

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();
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
}));
}

{
// 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());
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);
}));
}
94 changes: 86 additions & 8 deletions test/parallel/test-http2-client-destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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 });
Expand All @@ -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();

Expand All @@ -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
Expand All @@ -241,3 +240,82 @@ const Countdown = require('../common/countdown');
req.on('close', common.mustCall(() => server.close()));
}));
}

// Destroy ClientHttpSession with AbortSignal
{
const server = h2.createServer();
const controller = new AbortController();

server.on('stream', common.mustNotCall());
server.listen(0, common.mustCall(() => {
const { signal } = controller;
const client = h2.connect(`http://localhost:${server.address().port}`, {
signal,
});
client.on('close', common.mustCall());
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);

client.on('error', common.mustCall(common.mustCall((err) => {
assert.strictEqual(err.code, 'ABORT_ERR');
assert.strictEqual(err.name, 'AbortError');
})));

const req = client.request({}, {});
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);

req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_CANCEL');
assert.strictEqual(err.name, 'Error');
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
}));
req.on('close', common.mustCall(() => server.close()));

assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
// Signal listener attached
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);

controller.abort();
}));
}

// Destroy ClientHttpSession with AbortSignal
{
const server = h2.createSecureServer();
const controller = new AbortController();

server.on('stream', common.mustNotCall());
server.listen(0, common.mustCall(() => {
const { signal } = controller;
const client = h2.connect(`https://localhost:${server.address().port}`, {
signal,
});
client.on('close', common.mustCall());
console.log(getEventListeners(signal, 'abort').length);
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);

client.on('error', common.mustCall(common.mustCall((err) => {
assert.strictEqual(err.code, 'ABORT_ERR');
assert.strictEqual(err.name, 'AbortError');
})));

const req = client.request({}, {});
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);

req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_CANCEL');
assert.strictEqual(err.name, 'Error');
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
}));
req.on('close', common.mustCall(() => server.close()));

assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
// Signal listener attached
assert.strictEqual(getEventListeners(signal, 'abort').length, 1);

controller.abort();
}));
}
3 changes: 2 additions & 1 deletion test/parallel/test-https-abortcontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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');
Expand Down

0 comments on commit fca3195

Please sign in to comment.