http: handle aborts
This commit is contained in:
parent
c783aefb0f
commit
4733d0b1f0
@ -503,6 +503,10 @@ chunked, this will send the terminating `'0\r\n\r\n'`.
|
|||||||
If `data` is specified, it is equivalent to calling `request.write(data, encoding)`
|
If `data` is specified, it is equivalent to calling `request.write(data, encoding)`
|
||||||
followed by `request.end()`.
|
followed by `request.end()`.
|
||||||
|
|
||||||
|
### request.abort()
|
||||||
|
|
||||||
|
Aborts a request. (New since v0.3.80.)
|
||||||
|
|
||||||
|
|
||||||
## http.ClientResponse
|
## http.ClientResponse
|
||||||
|
|
||||||
|
66
lib/http.js
66
lib/http.js
@ -763,6 +763,25 @@ util.inherits(ClientRequest, OutgoingMessage);
|
|||||||
exports.ClientRequest = ClientRequest;
|
exports.ClientRequest = ClientRequest;
|
||||||
|
|
||||||
|
|
||||||
|
ClientRequest.prototype.abort = function() {
|
||||||
|
if (this._queue) {
|
||||||
|
// queued for dispatch
|
||||||
|
assert(!this.connection);
|
||||||
|
var i = this._queue.indexOf(this);
|
||||||
|
this._queue.splice(i, 1);
|
||||||
|
|
||||||
|
} else if (this.connection) {
|
||||||
|
// in-progress
|
||||||
|
var c = this.connection;
|
||||||
|
this.detachSocket(c);
|
||||||
|
c.destroy();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// already complete.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
function httpSocketSetup(socket) {
|
function httpSocketSetup(socket) {
|
||||||
// NOTE: be sure not to use ondrain elsewhere in this file!
|
// NOTE: be sure not to use ondrain elsewhere in this file!
|
||||||
socket.ondrain = function() {
|
socket.ondrain = function() {
|
||||||
@ -781,6 +800,11 @@ function Server(requestListener) {
|
|||||||
this.addListener('request', requestListener);
|
this.addListener('request', requestListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Similar option to this. Too lazy to write my own docs.
|
||||||
|
// http://www.squid-cache.org/Doc/config/half_closed_clients/
|
||||||
|
// http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
|
||||||
|
this.httpAllowHalfOpen = false;
|
||||||
|
|
||||||
this.addListener('connection', connectionListener);
|
this.addListener('connection', connectionListener);
|
||||||
}
|
}
|
||||||
util.inherits(Server, net.Server);
|
util.inherits(Server, net.Server);
|
||||||
@ -797,6 +821,15 @@ exports.createServer = function(requestListener) {
|
|||||||
function connectionListener(socket) {
|
function connectionListener(socket) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var outgoing = [];
|
var outgoing = [];
|
||||||
|
var incoming = [];
|
||||||
|
|
||||||
|
function abortIncoming() {
|
||||||
|
while (incoming.length) {
|
||||||
|
var req = incoming.shift();
|
||||||
|
req.emit('aborted');
|
||||||
|
}
|
||||||
|
// abort socket._httpMessage ?
|
||||||
|
}
|
||||||
|
|
||||||
debug('SERVER new http connection');
|
debug('SERVER new http connection');
|
||||||
|
|
||||||
@ -842,9 +875,18 @@ function connectionListener(socket) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
socket.onend = function() {
|
socket.onend = function() {
|
||||||
parser.finish();
|
var ret = parser.finish();
|
||||||
|
|
||||||
if (outgoing.length) {
|
if (ret instanceof Error) {
|
||||||
|
debug('parse error');
|
||||||
|
socket.destroy(ret);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self.httpAllowHalfOpen) {
|
||||||
|
abortIncoming();
|
||||||
|
socket.end();
|
||||||
|
} else if (outgoing.length) {
|
||||||
outgoing[outgoing.length - 1]._last = true;
|
outgoing[outgoing.length - 1]._last = true;
|
||||||
} else if (socket._httpMessage) {
|
} else if (socket._httpMessage) {
|
||||||
socket._httpMessage._last = true;
|
socket._httpMessage._last = true;
|
||||||
@ -854,14 +896,19 @@ function connectionListener(socket) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
socket.addListener('close', function() {
|
socket.addListener('close', function() {
|
||||||
|
debug('server socket close');
|
||||||
// unref the parser for easy gc
|
// unref the parser for easy gc
|
||||||
parsers.free(parser);
|
parsers.free(parser);
|
||||||
|
|
||||||
|
abortIncoming();
|
||||||
});
|
});
|
||||||
|
|
||||||
// The following callback is issued after the headers have been read on a
|
// The following callback is issued after the headers have been read on a
|
||||||
// new message. In this callback we setup the response object and pass it
|
// new message. In this callback we setup the response object and pass it
|
||||||
// to the user.
|
// to the user.
|
||||||
parser.onIncoming = function(req, shouldKeepAlive) {
|
parser.onIncoming = function(req, shouldKeepAlive) {
|
||||||
|
incoming.push(req);
|
||||||
|
|
||||||
var res = new ServerResponse(req);
|
var res = new ServerResponse(req);
|
||||||
debug('server response shouldKeepAlive: ' + shouldKeepAlive);
|
debug('server response shouldKeepAlive: ' + shouldKeepAlive);
|
||||||
res.shouldKeepAlive = shouldKeepAlive;
|
res.shouldKeepAlive = shouldKeepAlive;
|
||||||
@ -877,6 +924,9 @@ function connectionListener(socket) {
|
|||||||
// When we're finished writing the response, check if this is the last
|
// When we're finished writing the response, check if this is the last
|
||||||
// respose, if so destroy the socket.
|
// respose, if so destroy the socket.
|
||||||
res.on('finish', function() {
|
res.on('finish', function() {
|
||||||
|
assert(incoming[0] === req);
|
||||||
|
incoming.shift();
|
||||||
|
|
||||||
res.detachSocket(socket);
|
res.detachSocket(socket);
|
||||||
|
|
||||||
if (res._last) {
|
if (res._last) {
|
||||||
@ -909,23 +959,28 @@ function connectionListener(socket) {
|
|||||||
exports._connectionListener = connectionListener;
|
exports._connectionListener = connectionListener;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Agent(host, port) {
|
function Agent(host, port) {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
this.sockets = [];
|
this.sockets = [];
|
||||||
this.maxSockets = 5;
|
this.maxSockets = Agent.defaultMaxSockets;
|
||||||
}
|
}
|
||||||
util.inherits(Agent, EventEmitter);
|
util.inherits(Agent, EventEmitter);
|
||||||
exports.Agent = Agent;
|
exports.Agent = Agent;
|
||||||
|
|
||||||
|
|
||||||
|
Agent.defaultMaxSockets = 5;
|
||||||
|
|
||||||
|
|
||||||
Agent.prototype.appendMessage = function(options) {
|
Agent.prototype.appendMessage = function(options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var req = new ClientRequest(options);
|
var req = new ClientRequest(options);
|
||||||
this.queue.push(req);
|
this.queue.push(req);
|
||||||
|
req._queue = this.queue;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
req.on('finish', function () {
|
req.on('finish', function () {
|
||||||
@ -978,6 +1033,8 @@ Agent.prototype._establishNewConnection = function() {
|
|||||||
req = socket._httpMessage;
|
req = socket._httpMessage;
|
||||||
} else if (self.queue.length) {
|
} else if (self.queue.length) {
|
||||||
req = self.queue.shift();
|
req = self.queue.shift();
|
||||||
|
assert(req._queue === self.queue);
|
||||||
|
req._queue = null;
|
||||||
} else {
|
} else {
|
||||||
// No requests on queue? Where is the request
|
// No requests on queue? Where is the request
|
||||||
assert(0);
|
assert(0);
|
||||||
@ -1135,6 +1192,9 @@ Agent.prototype._cycle = function() {
|
|||||||
debug('Agent found socket, shift');
|
debug('Agent found socket, shift');
|
||||||
// We found an available connection!
|
// We found an available connection!
|
||||||
this.queue.shift(); // remove first from queue.
|
this.queue.shift(); // remove first from queue.
|
||||||
|
assert(first._queue === this.queue);
|
||||||
|
first._queue = null;
|
||||||
|
|
||||||
first.assignSocket(socket);
|
first.assignSocket(socket);
|
||||||
self._cycle(); // try to dispatch another
|
self._cycle(); // try to dispatch another
|
||||||
return;
|
return;
|
||||||
|
@ -754,6 +754,8 @@ Socket.prototype.destroy = function(exception) {
|
|||||||
// pool is shared between sockets, so don't need to free it here.
|
// pool is shared between sockets, so don't need to free it here.
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
debug('destroy ' + this.fd);
|
||||||
|
|
||||||
// TODO would like to set _writeQueue to null to avoid extra object alloc,
|
// TODO would like to set _writeQueue to null to avoid extra object alloc,
|
||||||
// but lots of code assumes this._writeQueue is always an array.
|
// but lots of code assumes this._writeQueue is always an array.
|
||||||
assert(this.bufferSize >= 0);
|
assert(this.bufferSize >= 0);
|
||||||
@ -787,6 +789,7 @@ Socket.prototype.destroy = function(exception) {
|
|||||||
|
|
||||||
// FIXME Bug when this.fd == 0
|
// FIXME Bug when this.fd == 0
|
||||||
if (typeof this.fd == 'number') {
|
if (typeof this.fd == 'number') {
|
||||||
|
debug('close ' + this.fd);
|
||||||
close(this.fd);
|
close(this.fd);
|
||||||
this.fd = null;
|
this.fd = null;
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
|
59
test/simple/test-http-client-abort.js
Normal file
59
test/simple/test-http-client-abort.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
var common = require('../common');
|
||||||
|
var assert = require('assert');
|
||||||
|
var http = require('http');
|
||||||
|
|
||||||
|
var clientAborts = 0;
|
||||||
|
|
||||||
|
var server = http.Server(function(req, res) {
|
||||||
|
console.log("Got connection");
|
||||||
|
res.writeHead(200);
|
||||||
|
res.write("Working on it...");
|
||||||
|
|
||||||
|
// I would expect an error event from req or res that the client aborted
|
||||||
|
// before completing the HTTP request / response cycle, or maybe a new
|
||||||
|
// event like "aborted" or something.
|
||||||
|
req.on("aborted", function () {
|
||||||
|
clientAborts++;
|
||||||
|
console.log("Got abort " + clientAborts);
|
||||||
|
if (clientAborts === N) {
|
||||||
|
console.log("All aborts detected, you win.");
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// since there is already clientError, maybe that would be appropriate,
|
||||||
|
// since "error" is magical
|
||||||
|
req.on("clientError", function () {
|
||||||
|
console.log("Got clientError");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var responses = 0;
|
||||||
|
var N = http.Agent.defaultMaxSockets - 1;
|
||||||
|
var requests = [];
|
||||||
|
|
||||||
|
server.listen(common.PORT, function() {
|
||||||
|
console.log("Server listening.");
|
||||||
|
|
||||||
|
for (var i = 0; i < N; i++) {
|
||||||
|
console.log("Making client " + i);
|
||||||
|
var options = { port: common.PORT, path: '/?id=' + i };
|
||||||
|
var req = http.get(options, function(res) {
|
||||||
|
console.log("Client response code " + res.statusCode);
|
||||||
|
|
||||||
|
if (++responses == N) {
|
||||||
|
console.log("All clients connected, destroying.");
|
||||||
|
requests.forEach(function (outReq) {
|
||||||
|
console.log("abort");
|
||||||
|
outReq.abort();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
requests.push(req);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('exit', function() {
|
||||||
|
assert.equal(N, clientAborts);
|
||||||
|
});
|
@ -48,6 +48,8 @@ var server = http.createServer(function(req, res) {
|
|||||||
});
|
});
|
||||||
server.listen(common.PORT);
|
server.listen(common.PORT);
|
||||||
|
|
||||||
|
server.httpAllowHalfOpen = true;
|
||||||
|
|
||||||
server.addListener('listening', function() {
|
server.addListener('listening', function() {
|
||||||
var c = net.createConnection(common.PORT);
|
var c = net.createConnection(common.PORT);
|
||||||
|
|
||||||
@ -69,6 +71,12 @@ server.addListener('listening', function() {
|
|||||||
if (requests_sent == 2) {
|
if (requests_sent == 2) {
|
||||||
c.write('GET / HTTP/1.1\r\nX-X: foo\r\n\r\n' +
|
c.write('GET / HTTP/1.1\r\nX-X: foo\r\n\r\n' +
|
||||||
'GET / HTTP/1.1\r\nX-X: bar\r\n\r\n');
|
'GET / HTTP/1.1\r\nX-X: bar\r\n\r\n');
|
||||||
|
// Note: we are making the connection half-closed here
|
||||||
|
// before we've gotten the response from the server. This
|
||||||
|
// is a pretty bad thing to do and not really supported
|
||||||
|
// by many http servers. Node supports it optionally if
|
||||||
|
// you set server.httpAllowHalfOpen=true, which we've done
|
||||||
|
// above.
|
||||||
c.end();
|
c.end();
|
||||||
assert.equal(c.readyState, 'readOnly');
|
assert.equal(c.readyState, 'readOnly');
|
||||||
requests_sent += 2;
|
requests_sent += 2;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user