Skip to content
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 {
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
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);
}