Skip to content

Commit 4733d0b

Browse files
committed
http: handle aborts
1 parent c783aef commit 4733d0b

File tree

5 files changed

+137
-3
lines changed

5 files changed

+137
-3
lines changed

doc/api/http.markdown

+4
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,10 @@ chunked, this will send the terminating `'0\r\n\r\n'`.
503503
If `data` is specified, it is equivalent to calling `request.write(data, encoding)`
504504
followed by `request.end()`.
505505

506+
### request.abort()
507+
508+
Aborts a request. (New since v0.3.80.)
509+
506510

507511
## http.ClientResponse
508512

lib/http.js

+63-3
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,25 @@ util.inherits(ClientRequest, OutgoingMessage);
763763
exports.ClientRequest = ClientRequest;
764764

765765

766+
ClientRequest.prototype.abort = function() {
767+
if (this._queue) {
768+
// queued for dispatch
769+
assert(!this.connection);
770+
var i = this._queue.indexOf(this);
771+
this._queue.splice(i, 1);
772+
773+
} else if (this.connection) {
774+
// in-progress
775+
var c = this.connection;
776+
this.detachSocket(c);
777+
c.destroy();
778+
779+
} else {
780+
// already complete.
781+
}
782+
};
783+
784+
766785
function httpSocketSetup(socket) {
767786
// NOTE: be sure not to use ondrain elsewhere in this file!
768787
socket.ondrain = function() {
@@ -781,6 +800,11 @@ function Server(requestListener) {
781800
this.addListener('request', requestListener);
782801
}
783802

803+
// Similar option to this. Too lazy to write my own docs.
804+
// http://www.squid-cache.org/Doc/config/half_closed_clients/
805+
// http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
806+
this.httpAllowHalfOpen = false;
807+
784808
this.addListener('connection', connectionListener);
785809
}
786810
util.inherits(Server, net.Server);
@@ -797,6 +821,15 @@ exports.createServer = function(requestListener) {
797821
function connectionListener(socket) {
798822
var self = this;
799823
var outgoing = [];
824+
var incoming = [];
825+
826+
function abortIncoming() {
827+
while (incoming.length) {
828+
var req = incoming.shift();
829+
req.emit('aborted');
830+
}
831+
// abort socket._httpMessage ?
832+
}
800833

801834
debug('SERVER new http connection');
802835

@@ -842,9 +875,18 @@ function connectionListener(socket) {
842875
};
843876

844877
socket.onend = function() {
845-
parser.finish();
878+
var ret = parser.finish();
846879

847-
if (outgoing.length) {
880+
if (ret instanceof Error) {
881+
debug('parse error');
882+
socket.destroy(ret);
883+
return;
884+
}
885+
886+
if (!self.httpAllowHalfOpen) {
887+
abortIncoming();
888+
socket.end();
889+
} else if (outgoing.length) {
848890
outgoing[outgoing.length - 1]._last = true;
849891
} else if (socket._httpMessage) {
850892
socket._httpMessage._last = true;
@@ -854,14 +896,19 @@ function connectionListener(socket) {
854896
};
855897

856898
socket.addListener('close', function() {
899+
debug('server socket close');
857900
// unref the parser for easy gc
858901
parsers.free(parser);
902+
903+
abortIncoming();
859904
});
860905

861906
// The following callback is issued after the headers have been read on a
862907
// new message. In this callback we setup the response object and pass it
863908
// to the user.
864909
parser.onIncoming = function(req, shouldKeepAlive) {
910+
incoming.push(req);
911+
865912
var res = new ServerResponse(req);
866913
debug('server response shouldKeepAlive: ' + shouldKeepAlive);
867914
res.shouldKeepAlive = shouldKeepAlive;
@@ -877,6 +924,9 @@ function connectionListener(socket) {
877924
// When we're finished writing the response, check if this is the last
878925
// respose, if so destroy the socket.
879926
res.on('finish', function() {
927+
assert(incoming[0] === req);
928+
incoming.shift();
929+
880930
res.detachSocket(socket);
881931

882932
if (res._last) {
@@ -909,23 +959,28 @@ function connectionListener(socket) {
909959
exports._connectionListener = connectionListener;
910960

911961

962+
912963
function Agent(host, port) {
913964
this.host = host;
914965
this.port = port;
915966

916967
this.queue = [];
917968
this.sockets = [];
918-
this.maxSockets = 5;
969+
this.maxSockets = Agent.defaultMaxSockets;
919970
}
920971
util.inherits(Agent, EventEmitter);
921972
exports.Agent = Agent;
922973

923974

975+
Agent.defaultMaxSockets = 5;
976+
977+
924978
Agent.prototype.appendMessage = function(options) {
925979
var self = this;
926980

927981
var req = new ClientRequest(options);
928982
this.queue.push(req);
983+
req._queue = this.queue;
929984

930985
/*
931986
req.on('finish', function () {
@@ -978,6 +1033,8 @@ Agent.prototype._establishNewConnection = function() {
9781033
req = socket._httpMessage;
9791034
} else if (self.queue.length) {
9801035
req = self.queue.shift();
1036+
assert(req._queue === self.queue);
1037+
req._queue = null;
9811038
} else {
9821039
// No requests on queue? Where is the request
9831040
assert(0);
@@ -1135,6 +1192,9 @@ Agent.prototype._cycle = function() {
11351192
debug('Agent found socket, shift');
11361193
// We found an available connection!
11371194
this.queue.shift(); // remove first from queue.
1195+
assert(first._queue === this.queue);
1196+
first._queue = null;
1197+
11381198
first.assignSocket(socket);
11391199
self._cycle(); // try to dispatch another
11401200
return;

lib/net.js

+3
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,8 @@ Socket.prototype.destroy = function(exception) {
754754
// pool is shared between sockets, so don't need to free it here.
755755
var self = this;
756756

757+
debug('destroy ' + this.fd);
758+
757759
// TODO would like to set _writeQueue to null to avoid extra object alloc,
758760
// but lots of code assumes this._writeQueue is always an array.
759761
assert(this.bufferSize >= 0);
@@ -787,6 +789,7 @@ Socket.prototype.destroy = function(exception) {
787789

788790
// FIXME Bug when this.fd == 0
789791
if (typeof this.fd == 'number') {
792+
debug('close ' + this.fd);
790793
close(this.fd);
791794
this.fd = null;
792795
process.nextTick(function() {

test/simple/test-http-client-abort.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
var common = require('../common');
2+
var assert = require('assert');
3+
var http = require('http');
4+
5+
var clientAborts = 0;
6+
7+
var server = http.Server(function(req, res) {
8+
console.log("Got connection");
9+
res.writeHead(200);
10+
res.write("Working on it...");
11+
12+
// I would expect an error event from req or res that the client aborted
13+
// before completing the HTTP request / response cycle, or maybe a new
14+
// event like "aborted" or something.
15+
req.on("aborted", function () {
16+
clientAborts++;
17+
console.log("Got abort " + clientAborts);
18+
if (clientAborts === N) {
19+
console.log("All aborts detected, you win.");
20+
server.close();
21+
}
22+
});
23+
24+
// since there is already clientError, maybe that would be appropriate,
25+
// since "error" is magical
26+
req.on("clientError", function () {
27+
console.log("Got clientError");
28+
});
29+
});
30+
31+
var responses = 0;
32+
var N = http.Agent.defaultMaxSockets - 1;
33+
var requests = [];
34+
35+
server.listen(common.PORT, function() {
36+
console.log("Server listening.");
37+
38+
for (var i = 0; i < N; i++) {
39+
console.log("Making client " + i);
40+
var options = { port: common.PORT, path: '/?id=' + i };
41+
var req = http.get(options, function(res) {
42+
console.log("Client response code " + res.statusCode);
43+
44+
if (++responses == N) {
45+
console.log("All clients connected, destroying.");
46+
requests.forEach(function (outReq) {
47+
console.log("abort");
48+
outReq.abort();
49+
});
50+
}
51+
});
52+
53+
requests.push(req);
54+
}
55+
});
56+
57+
process.on('exit', function() {
58+
assert.equal(N, clientAborts);
59+
});

test/simple/test-http-server.js

+8
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ var server = http.createServer(function(req, res) {
4848
});
4949
server.listen(common.PORT);
5050

51+
server.httpAllowHalfOpen = true;
52+
5153
server.addListener('listening', function() {
5254
var c = net.createConnection(common.PORT);
5355

@@ -69,6 +71,12 @@ server.addListener('listening', function() {
6971
if (requests_sent == 2) {
7072
c.write('GET / HTTP/1.1\r\nX-X: foo\r\n\r\n' +
7173
'GET / HTTP/1.1\r\nX-X: bar\r\n\r\n');
74+
// Note: we are making the connection half-closed here
75+
// before we've gotten the response from the server. This
76+
// is a pretty bad thing to do and not really supported
77+
// by many http servers. Node supports it optionally if
78+
// you set server.httpAllowHalfOpen=true, which we've done
79+
// above.
7280
c.end();
7381
assert.equal(c.readyState, 'readOnly');
7482
requests_sent += 2;

0 commit comments

Comments
 (0)