Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: align OutgoingMessage and ClientRequest destroy #32148

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 42 additions & 24 deletions lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const {
ObjectAssign,
ObjectKeys,
ObjectSetPrototypeOf,
Symbol
} = primordials;

const net = require('net');
Expand Down Expand Up @@ -65,6 +66,7 @@ const {
} = require('internal/dtrace');

const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
const kOnSocket = Symbol('kOnSocket');

function validateHost(host, name) {
if (host !== null && host !== undefined && typeof host !== 'string') {
Expand Down Expand Up @@ -337,25 +339,47 @@ ClientRequest.prototype._implicitHeader = function _implicitHeader() {
};

ClientRequest.prototype.abort = function abort() {
if (!this.aborted) {
process.nextTick(emitAbortNT, this);
if (this.aborted) {
return;
}
this.aborted = true;
process.nextTick(emitAbortNT, this);
this.destroy();
};

ClientRequest.prototype.destroy = function destroy(err) {
if (this.destroyed) {
return;
}
this.destroyed = true;

// If we're aborting, we don't care about any more response data.
if (this.res) {
this.res._dump();
ronag marked this conversation as resolved.
Show resolved Hide resolved
}

// In the event that we don't have a socket, we will pop out of
// the request queue through handling in onSocket.
if (this.socket) {
// in-progress
this.socket.destroy();
if (!this.socket) {
this.once(kOnSocket, (socket) => {
ronag marked this conversation as resolved.
Show resolved Hide resolved
if (this.agent) {
socket.emit('free');
} else {
socket.destroy();
}

if (!this.aborted && !err) {
err = connResetException('socket hang up');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, destroy() will emit ECONNRESET while abort won't.

}

if (err) {
this.emit('error', err);
}
this.emit('close');
});
} else {
this.socket.destroy(err);
}
};


function emitAbortNT(req) {
req.emit('abort');
}
Expand Down Expand Up @@ -425,7 +449,9 @@ function socketErrorListener(err) {
const req = socket._httpMessage;
debug('SOCKET ERROR:', err.message, err.stack);

if (req) {
// If writableFinished then the error came from the readable/response
// side and will be emitted there.
if (req && !req.writableFinished) {
// For Safety. Some additional errors might fire later on
// and we need to make sure we don't double-fire the error event.
req.socket._hadError = true;
Expand Down Expand Up @@ -686,6 +712,12 @@ function emitFreeNT(req) {
}

function tickOnSocket(req, socket) {
req.emit(kOnSocket, socket);

if (req.destroyed) {
return;
}

const parser = parsers.alloc();
req.socket = socket;
parser.initialize(HTTPParser.RESPONSE,
Expand Down Expand Up @@ -746,23 +778,9 @@ function listenSocketTimeout(req) {
}

ClientRequest.prototype.onSocket = function onSocket(socket) {
process.nextTick(onSocketNT, this, socket);
process.nextTick(tickOnSocket, this, socket);
};

function onSocketNT(req, socket) {
if (req.aborted) {
// If we were aborted while waiting for a socket, skip the whole thing.
if (!req.agent) {
socket.destroy();
} else {
req.emit('close');
socket.emit('free');
}
} else {
tickOnSocket(req, socket);
}
}

ClientRequest.prototype._deferToConnect = _deferToConnect;
function _deferToConnect(method, arguments_, cb) {
// This function is for calls that need to happen once the socket is
Expand Down
6 changes: 6 additions & 0 deletions lib/_http_outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function OutgoingMessage() {
this.outputSize = 0;

this.writable = true;
this.destroyed = false;

this._last = false;
this.chunkedEncoding = false;
Expand Down Expand Up @@ -277,6 +278,11 @@ OutgoingMessage.prototype.setTimeout = function setTimeout(msecs, callback) {
// any messages, before ever calling this. In that case, just skip
// it, since something else is destroying this connection anyway.
OutgoingMessage.prototype.destroy = function destroy(error) {
if (this.destroyed) {
return;
}
this.destroyed = true;

if (this.socket) {
this.socket.destroy(error);
} else {
ronag marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
1 change: 0 additions & 1 deletion lib/internal/streams/destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ function isRequest(stream) {

// Normalize destroy for legacy.
function destroyer(stream, err) {
// request.destroy just do .end - .abort is what we want
mcollina marked this conversation as resolved.
Show resolved Hide resolved
if (isRequest(stream)) return stream.abort();
if (isRequest(stream.req)) return stream.req.abort();
if (typeof stream.destroy === 'function') return stream.destroy(err);
Expand Down
71 changes: 71 additions & 0 deletions test/parallel/test-http-client-abort-destroy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use strict';
const common = require('../common');
const http = require('http');
const assert = require('assert');

{
// abort

const server = http.createServer(common.mustCall((req, res) => {
res.end('Hello');
}));

server.listen(0, common.mustCall(() => {
const options = { port: server.address().port };
const req = http.get(options, common.mustCall((res) => {
res.on('data', (data) => {
req.abort();
assert.strictEqual(req.aborted, true);
assert.strictEqual(req.destroyed, true);
server.close();
});
}));
req.on('error', common.mustNotCall());
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
}));
}

{
// destroy + res

const server = http.createServer(common.mustCall((req, res) => {
res.end('Hello');
}));

server.listen(0, common.mustCall(() => {
const options = { port: server.address().port };
const req = http.get(options, common.mustCall((res) => {
res.on('data', (data) => {
req.destroy();
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
server.close();
});
}));
req.on('error', common.mustNotCall());
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
}));
}


{
// destroy

const server = http.createServer(common.mustNotCall((req, res) => {
}));

server.listen(0, common.mustCall(() => {
const options = { port: server.address().port };
const req = http.get(options, common.mustNotCall());
req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ECONNRESET');
}));
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, false);
req.destroy();
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.destroyed, true);
}));
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,45 @@ const common = require('../common');
const assert = require('assert');
const http = require('http');

let socketsCreated = 0;

class Agent extends http.Agent {
createConnection(options, oncreate) {
const socket = super.createConnection(options, oncreate);
socketsCreated++;
return socket;
for (const destroyer of ['destroy', 'abort']) {
let socketsCreated = 0;

class Agent extends http.Agent {
createConnection(options, oncreate) {
const socket = super.createConnection(options, oncreate);
socketsCreated++;
return socket;
}
}
}

const server = http.createServer((req, res) => res.end());
const server = http.createServer((req, res) => res.end());

server.listen(0, common.mustCall(() => {
const port = server.address().port;
const agent = new Agent({
keepAlive: true,
maxSockets: 1
});
server.listen(0, common.mustCall(() => {
const port = server.address().port;
const agent = new Agent({
keepAlive: true,
maxSockets: 1
});

http.get({ agent, port }, (res) => res.resume());
http.get({ agent, port }, (res) => res.resume());

const req = http.get({ agent, port }, common.mustNotCall());
req.abort();
const req = http.get({ agent, port }, common.mustNotCall());
req[destroyer]();

http.get({ agent, port }, common.mustCall((res) => {
res.resume();
assert.strictEqual(socketsCreated, 1);
agent.destroy();
server.close();
if (destroyer === 'destroy') {
req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ECONNRESET');
}));
} else {
req.on('error', common.mustNotCall());
}

http.get({ agent, port }, common.mustCall((res) => {
res.resume();
assert.strictEqual(socketsCreated, 1);
agent.destroy();
server.close();
}));
}));
}));
}
4 changes: 2 additions & 2 deletions test/parallel/test-http-client-close-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ server.listen(0, common.mustCall(() => {
const req = http.get({ port: server.address().port }, common.mustNotCall());
let errorEmitted = false;

req.on('error', (err) => {
req.on('error', common.mustCall((err) => {
errorEmitted = true;
assert.strictEqual(err.constructor, Error);
assert.strictEqual(err.message, 'socket hang up');
assert.strictEqual(err.code, 'ECONNRESET');
});
}));

req.on('close', common.mustCall(() => {
assert.strictEqual(errorEmitted, true);
Expand Down
7 changes: 7 additions & 0 deletions test/parallel/test-http-outgoing-proto.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,10 @@ assert.throws(() => {
name: 'TypeError',
message: 'Invalid character in trailer content ["404"]'
});

{
const outgoingMessage = new OutgoingMessage();
assert.strictEqual(outgoingMessage.destroyed, false);
outgoingMessage.destroy();
assert.strictEqual(outgoingMessage.destroyed, true);
}