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 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
47 changes: 35 additions & 12 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 kError = Symbol('kError');

function validateHost(host, name) {
if (host !== null && host !== undefined && typeof host !== 'string') {
Expand Down Expand Up @@ -337,10 +339,19 @@ 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) {
Expand All @@ -350,11 +361,29 @@ ClientRequest.prototype.abort = function abort() {
// 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();
_destroy(this, this.socket, err);
} else if (err) {
this[kError] = err;
}
};

function _destroy(req, socket, err) {
// TODO (ronag): Check if socket was used at all (e.g. headersSent) and
// re-use it in that case. `req.socket` just checks whether the socket was
// assigned to the request and *might* have been used.
if (!req.agent || req.socket) {
socket.destroy(err);
} else {
socket.emit('free');
if (!req.aborted && !err) {
err = connResetException('socket hang up');
}
if (err) {
req.emit('error', err);
}
req.emit('close');
}
}

function emitAbortNT(req) {
req.emit('abort');
Expand Down Expand Up @@ -750,14 +779,8 @@ ClientRequest.prototype.onSocket = function onSocket(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');
}
if (req.destroyed) {
_destroy(req, socket, req[kError]);
} else {
tickOnSocket(req, socket);
}
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');
server.close();
}));
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);
}