Browse Source

http: handle aborts

v0.7.4-release
Ryan Dahl 14 years ago
parent
commit
4733d0b1f0
  1. 4
      doc/api/http.markdown
  2. 66
      lib/http.js
  3. 3
      lib/net.js
  4. 59
      test/simple/test-http-client-abort.js
  5. 8
      test/simple/test-http-server.js

4
doc/api/http.markdown

@ -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

@ -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;

3
lib/net.js

@ -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

@ -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);
});

8
test/simple/test-http-server.js

@ -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…
Cancel
Save