Browse Source

Merge branch 'http_agent'

v0.7.4-release
Ryan Dahl 14 years ago
parent
commit
6eca6b1ec0
  1. 149
      doc/api/http.markdown
  2. 649
      lib/http.js
  3. 1
      src/node_http_parser.cc
  4. 6
      test/pummel/test-http-client-reconnect-bug.js
  5. 6
      test/pummel/test-http-upload-timeout.js
  6. 12
      test/simple/test-http-set-timeout.js
  7. 3
      test/simple/test-http-upgrade-client2.js

149
doc/api/http.markdown

@ -22,7 +22,6 @@ HTTP API is very low-level. It deals with stream handling and message
parsing only. It parses a message into headers and body but it does not
parse the actual headers or the body.
HTTPS is supported if OpenSSL is available on the underlying platform.
## http.Server
@ -311,34 +310,57 @@ If `data` is specified, it is equivalent to calling `response.write(data, encodi
followed by `response.end()`.
## http.Client
## http.request(options, callback)
An HTTP client is constructed with a server address as its
argument, the returned handle is then used to issue one or more
requests. Depending on the server connected to, the client might
pipeline the requests or reestablish the stream after each
stream. _Currently the implementation does not pipeline requests._
Node maintains several connections per server to make HTTP requests.
This function allows one to transparently issue requests.
Example of connecting to `google.com`:
Options:
var http = require('http');
var google = http.createClient(80, 'www.google.com');
var request = google.request('GET', '/',
{'host': 'www.google.com'});
request.end();
request.on('response', function (response) {
console.log('STATUS: ' + response.statusCode);
console.log('HEADERS: ' + JSON.stringify(response.headers));
response.setEncoding('utf8');
response.on('data', function (chunk) {
- `host`: A domain name or IP address of the server to issue the request to.
- `port`: Port of remote server.
- `method`: A string specifing the HTTP request method. Possible values:
`'GET'` (default), `'POST'`, `'PUT'`, and `'DELETE'`.
- `path`: Request path. Should include query string and fragments if any.
E.G. `'/index.html?page=12'`
- `headers`: An object containing request headers.
`http.request()` returns an instance of the `http.ClientRequest`
class. The `ClientRequest` instance is a writable stream. If one needs to
upload a file with a POST request, then write to the `ClientRequest` object.
Example:
var options = {
host: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST'
};
var req = http.request(options, function(res) {
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('BODY: ' + chunk);
});
});
There are a few special headers that should be noted.
// write data to request body
req.write('data\n');
req.write('data\n');
req.end();
Note that in the example `req.end()` was called. With `http.request()` one
must always call `req.end()` to signify that you're done with the request -
even if there is no data being written to the request body.
* The 'Host' header is not added by Node, and is usually required by
website.
If any error is encountered during the request (be that with DNS resolution,
TCP level errors, or actual HTTP parse errors) an `'error'` event is emitted
on the returned request object.
There are a few special headers that should be noted.
* Sending a 'Connection: keep-alive' will notify Node that the connection to
the server should be persisted until the next request.
@ -350,6 +372,33 @@ There are a few special headers that should be noted.
and listen for the `continue` event. See RFC2616 Section 8.2.3 for more
information.
## http.get(options, callback)
Since most requests are GET requests without bodies, Node provides this
convience method. The only difference between this method and `http.request()` is
that it sets the method to GET and calls `req.end()` automatically.
Example:
var options = {
host: 'www.google.com',
port: 80,
path: '/index.html'
};
http.get(options, function(res) {
console.log("Got response: " + res.statusCode);
}).on('error', function(e) {
console.log("Got error: " + e.message);
});
## http.Agent
`http.request()` uses a special `Agent` for managing multiple connections to
an HTTP server. Normally `Agent` instances should not be exposed to user
code, however in certain situations it's useful to check the status of the
agent.
### Event: 'upgrade'
@ -369,56 +418,24 @@ Emitted when the server sends a '100 Continue' HTTP response, usually because
the request contained 'Expect: 100-continue'. This is an instruction that
the client should send the request body.
### agent.maxSockets
### http.createClient(port, host='localhost', secure=false, [credentials])
Constructs a new HTTP client. `port` and
`host` refer to the server to be connected to. A
stream is not established until a request is issued.
`secure` is an optional boolean flag to enable https support and `credentials` is an optional
credentials object from the crypto module, which may hold the client's private key,
certificate, and a list of trusted CA certificates.
If the connection is secure, but no explicit CA certificates are passed
in the credentials, then node.js will default to the publicly trusted list
of CA certificates, as given in <http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt>.
By default set to 5. Determines how many concurrent sockets the agent can have open.
### client.request(method='GET', path, [request_headers])
### agent.sockets
Issues a request; if necessary establishes stream. Returns a `http.ClientRequest` instance.
An array of sockets currently inuse by the Agent. Do not modify.
`method` is optional and defaults to 'GET' if omitted.
### agent.queue
`request_headers` is optional.
Additional request headers might be added internally
by Node. Returns a `ClientRequest` object.
A queue of requests waiting to be sent to sockets.
Do remember to include the `Content-Length` header if you
plan on sending a body. If you plan on streaming the body, perhaps
set `Transfer-Encoding: chunked`.
*NOTE*: the request is not complete. This method only sends the header of
the request. One needs to call `request.end()` to finalize the request and
retrieve the response. (This sounds convoluted but it provides a chance for
the user to stream a body to the server with `request.write()`.)
### client.verifyPeer()
Returns true or false depending on the validity of the server's certificate
in the context of the defined or default list of trusted CA certificates.
### client.getPeerCertificate()
Returns a JSON structure detailing the server's certificate, containing a dictionary
with keys for the certificate `'subject'`, `'issuer'`, `'valid_from'` and `'valid_to'`.
## http.ClientRequest
This object is created internally and returned from the `request()` method
of a `http.Client`. It represents an _in-progress_ request whose header has
already been sent.
This object is created internally and returned from `http.request()`. It
represents an _in-progress_ request whose header has already been sent.
To get the response, add a listener for `'response'` to the request object.
`'response'` will be emitted from the request object when the response
@ -488,7 +505,7 @@ followed by `request.end()`.
## http.ClientResponse
This object is created when making a request with `http.Client`. It is
This object is created when making a request with `http.request()`. It is
passed to the `'response'` event of the request object.
The response implements the `Readable Stream` interface.
@ -499,10 +516,6 @@ The response implements the `Readable Stream` interface.
Emitted when a piece of the message body is received.
Example: A chunk of the body is given as the single
argument. The transfer-encoding has been decoded. The
body chunk a String. The body encoding is set with
`response.setBodyEncoding()`.
### Event: 'end'
@ -542,7 +555,3 @@ Pauses response from emitting events. Useful to throttle back a download.
### response.resume()
Resumes a paused response.
### response.client
A reference to the `http.Client` that this response belongs to.

649
lib/http.js

@ -1,8 +1,10 @@
var util = require('util');
var net = require('net');
var stream = require('stream');
var EventEmitter = require('events').EventEmitter;
var FreeList = require('freelist').FreeList;
var HTTPParser = process.binding('http_parser').HTTPParser;
var assert = process.assert;
var debug;
@ -284,13 +286,9 @@ IncomingMessage.prototype._addHeaderLine = function(field, value) {
};
function OutgoingMessage(socket) {
function OutgoingMessage() {
stream.Stream.call(this);
// TODO Remove one of these eventually.
this.socket = socket;
this.connection = socket;
this.output = [];
this.outputEncodings = [];
@ -312,6 +310,22 @@ util.inherits(OutgoingMessage, stream.Stream);
exports.OutgoingMessage = OutgoingMessage;
OutgoingMessage.prototype.assignSocket = function(socket) {
assert(!socket._httpMessage);
socket._httpMessage = this;
this.socket = socket;
this.connection = socket;
this._flush();
};
OutgoingMessage.prototype.detachSocket = function(socket) {
assert(socket._httpMessage == this);
socket._httpMessage = null;
this.socket = this.connection = null;
};
OutgoingMessage.prototype.destroy = function(error) {
this.socket.destroy(error);
};
@ -336,7 +350,9 @@ OutgoingMessage.prototype._send = function(data, encoding) {
OutgoingMessage.prototype._writeRaw = function(data, encoding) {
if (this.connection._outgoing[0] === this && this.connection.writable) {
if (this.connection &&
this.connection._httpMessage === this &&
this.connection.writable) {
// There might be pending data in the this.output buffer.
while (this.output.length) {
if (!this.connection.writable) {
@ -550,7 +566,7 @@ OutgoingMessage.prototype.end = function(data, encoding) {
data.length > 0 &&
this.output.length === 0 &&
this.connection.writable &&
this.connection._outgoing[0] === this;
this.connection._httpMessage === this;
if (hot) {
// Hot path. They're doing
@ -585,17 +601,67 @@ OutgoingMessage.prototype.end = function(data, encoding) {
// There is the first message on the outgoing queue, and we've sent
// everything to the socket.
if (this.output.length === 0 && this.connection._outgoing[0] === this) {
debug('outgoing message end. shifting because was flushed');
this.connection._onOutgoingSent();
if (this.output.length === 0 && this.connection._httpMessage === this) {
debug('outgoing message end.');
this._finish();
}
return ret;
};
OutgoingMessage.prototype._finish = function() {
this.emit('finish');
};
OutgoingMessage.prototype._flush = function() {
// This logic is probably a bit confusing. Let me explain a bit:
//
// In both HTTP servers and clients it is possible to queue up several
// outgoing messages. This is easiest to imagine in the case of a client.
// Take the following situation:
//
// req1 = client.request('GET', '/');
// req2 = client.request('POST', '/');
//
// When the user does
//
// req2.write('hello world\n');
//
// it's possible that the first request has not been completely flushed to
// the socket yet. Thus the outgoing messages need to be prepared to queue
// up data internally before sending it on further to the socket's queue.
//
// This function, outgoingFlush(), is called by both the Server and Client
// to attempt to flush any pending messages out to the socket.
if (!this.socket) return;
var ret;
while (this.output.length) {
if (!this.socket.writable) return; // XXX Necessary?
var data = this.output.shift();
var encoding = this.outputEncodings.shift();
ret = this.socket.write(data, encoding);
}
if (this.finished) {
// This is a queue to the server or client to bring in the next this.
this._finish();
} else if (ret) {
this.emit('drain');
}
};
function ServerResponse(req) {
OutgoingMessage.call(this, req.socket);
OutgoingMessage.call(this);
if (req.method === 'HEAD') this._hasBody = false;
@ -666,19 +732,30 @@ ServerResponse.prototype.writeHeader = function() {
};
function ClientRequest(socket, method, url, headers) {
OutgoingMessage.call(this, socket);
function ClientRequest(options) {
OutgoingMessage.call(this);
var method = this.method = (options.method || 'GET').toUpperCase();
var path = options.path || '/';
var headers = options.headers || {};
// Host header set by default.
if (options.host && !(headers.host || headers.Host || headers.HOST)) {
headers.Host = options.host;
}
this.method = method = method.toUpperCase();
this.shouldKeepAlive = false;
if (method === 'GET' || method === 'HEAD') {
this.useChunkedEncodingByDefault = false;
} else {
this.useChunkedEncodingByDefault = true;
}
// By default keep-alive is off. This is the last message unless otherwise
// specified.
this._last = true;
this._storeHeader(method + ' ' + url + ' HTTP/1.1\r\n', headers);
this._storeHeader(method + ' ' + path + ' HTTP/1.1\r\n', headers);
}
util.inherits(ClientRequest, OutgoingMessage);
@ -686,58 +763,12 @@ util.inherits(ClientRequest, OutgoingMessage);
exports.ClientRequest = ClientRequest;
function outgoingFlush(socket) {
// This logic is probably a bit confusing. Let me explain a bit:
//
// In both HTTP servers and clients it is possible to queue up several
// outgoing messages. This is easiest to imagine in the case of a client.
// Take the following situation:
//
// req1 = client.request('GET', '/');
// req2 = client.request('POST', '/');
//
// When the user does
//
// req2.write('hello world\n');
//
// it's possible that the first request has not been completely flushed to
// the socket yet. Thus the outgoing messages need to be prepared to queue
// up data internally before sending it on further to the socket's queue.
//
// This function, outgoingFlush(), is called by both the Server and Client
// to attempt to flush any pending messages out to the socket.
var message = socket._outgoing[0];
if (!message) return;
var ret;
while (message.output.length) {
if (!socket.writable) return; // XXX Necessary?
var data = message.output.shift();
var encoding = message.outputEncodings.shift();
ret = socket.write(data, encoding);
}
if (message.finished) {
socket._onOutgoingSent();
} else if (ret) {
message.emit('drain');
}
}
function httpSocketSetup(socket) {
// An array of outgoing messages for the socket. In pipelined connections
// we need to keep track of the order they were sent.
socket._outgoing = [];
// NOTE: be sure not to use ondrain elsewhere in this file!
socket.ondrain = function() {
var message = socket._outgoing[0];
if (message) message.emit('drain');
if (socket._httpMessage) {
socket._httpMessage.emit('drain');
}
};
}
@ -765,6 +796,7 @@ exports.createServer = function(requestListener) {
function connectionListener(socket) {
var self = this;
var outgoing = [];
debug('SERVER new http connection');
@ -778,6 +810,7 @@ function connectionListener(socket) {
var parser = parsers.alloc();
parser.reinitialize('request');
parser.socket = socket;
parser.incoming = null;
socket.addListener('error', function(e) {
self.emit('clientError', e);
@ -811,9 +844,10 @@ function connectionListener(socket) {
socket.onend = function() {
parser.finish();
if (socket._outgoing.length) {
socket._outgoing[socket._outgoing.length - 1]._last = true;
outgoingFlush(socket);
if (outgoing.length) {
outgoing[outgoing.length - 1]._last = true;
} else if (socket._httpMessage) {
socket._httpMessage._last = true;
} else {
socket.end();
}
@ -824,21 +858,6 @@ function connectionListener(socket) {
parsers.free(parser);
});
// At the end of each response message, after it has been flushed to the
// socket. Here we insert logic about what to do next.
socket._onOutgoingSent = function(message) {
var message = socket._outgoing.shift();
if (message._last) {
// No more messages to be pushed out.
socket.destroySoon();
} else if (socket._outgoing.length) {
// Push out the next message.
outgoingFlush(socket);
}
};
// 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
// to the user.
@ -846,7 +865,29 @@ function connectionListener(socket) {
var res = new ServerResponse(req);
debug('server response shouldKeepAlive: ' + shouldKeepAlive);
res.shouldKeepAlive = shouldKeepAlive;
socket._outgoing.push(res);
if (socket._httpMessage) {
// There are already pending outgoing res, append.
outgoing.push(res);
} else {
res.assignSocket(socket);
}
// When we're finished writing the response, check if this is the last
// respose, if so destroy the socket.
res.on('finish', function() {
res.detachSocket(socket);
if (res._last) {
socket.destroySoon();
} else {
// start sending the next message
var m = outgoing.shift();
if (m) {
m.assignSocket(socket);
}
}
});
if ('expect' in req.headers &&
(req.httpVersionMajor == 1 && req.httpVersionMinor == 1) &&
@ -867,109 +908,153 @@ function connectionListener(socket) {
exports._connectionListener = connectionListener;
function Client() {
if (!(this instanceof Client)) return new Client();
net.Stream.call(this, { allowHalfOpen: true });
function Agent(host, port) {
this.host = host;
this.port = port;
this.queue = [];
this.sockets = [];
this.maxSockets = 5;
}
util.inherits(Agent, EventEmitter);
Agent.prototype.appendMessage = function(options) {
var self = this;
// Possible states:
// - disconnected
// - connecting
// - connected
this._state = 'disconnected';
var req = new ClientRequest(options);
this.queue.push(req);
httpSocketSetup(self);
/*
req.on('finish', function () {
self._cycle();
});
*/
function onData(d, start, end) {
if (!self.parser) {
throw new Error('parser not initialized prior to Client.ondata call');
}
var ret = self.parser.execute(d, start, end - start);
if (ret instanceof Error) {
self.destroy(ret);
} else if (self.parser.incoming && self.parser.incoming.upgrade) {
var bytesParsed = ret;
self.ondata = null;
self.onend = null;
this._cycle();
var req = self.parser.incoming;
return req;
};
var upgradeHead = d.slice(start + bytesParsed + 1, end);
if (self.listeners('upgrade').length) {
self.emit('upgrade', req, self, upgradeHead);
} else {
self.destroy();
}
}
};
Agent.prototype._removeSocket = function(socket) {
var i = this.sockets.indexOf(socket);
if (i >= 0) this.sockets.splice(i, 1);
}
self.addListener('connect', function() {
debug('CLIENT connected');
self.ondata = onData;
self.onend = onEnd;
Agent.prototype._establishNewConnection = function() {
var self = this;
assert(this.sockets.length < this.maxSockets);
// Grab a new "socket". Depending on the implementation of _getConnection
// this could either be a raw TCP socket or a TLS stream.
var socket = this._getConnection(this.host, this.port, function () {
self.emit('connect'); // mostly for the shim.
debug("Agent _getConnection callback");
self._cycle();
});
this.sockets.push(socket);
self._state = 'connected';
// Add a parser to the socket.
var parser = parsers.alloc();
parser.reinitialize('response');
parser.socket = socket;
parser.incoming = null;
socket.on('error', function(err) {
debug("AGENT SOCKET ERROR: " + err.message);
var req;
if (socket._httpMessage) {
req = socket._httpMessage
} else if (self.queue.length) {
req = self.queue.shift();
} else {
// No requests on queue? Where is the request
assert(0);
}
self._initParser();
outgoingFlush(self);
req.emit('error', err);
req._hadError = true; // hacky
});
function onEnd() {
if (self.parser) self.parser.finish();
debug('CLIENT got end closing. state = ' + self._state);
self.end();
};
socket.ondata = function(d, start, end) {
var ret = parser.execute(d, start, end - start);
if (ret instanceof Error) {
debug('parse error');
socket.destroy(ret);
} else if (parser.incoming && parser.incoming.upgrade) {
var bytesParsed = ret;
socket.ondata = null;
socket.onend = null;
self.addListener('close', function(e) {
self._state = 'disconnected';
if (e) return;
var res = parser.incoming;
assert(socket._httpMessage);
socket._httpMessage.res = res;
debug('CLIENT onClose. state = ' + self._state);
// This is start + byteParsed + 1 due to the error of getting \n
// in the upgradeHead from the closing lines of the headers
var upgradeHead = d.slice(start + bytesParsed + 1, end);
// finally done with the request
self._outgoing.shift();
// Make sure we don't try to send HTTP requests to it.
self._removeSocket(socket);
// If there are more requests to handle, reconnect.
if (self._outgoing.length) {
self._ensureConnection();
} else if (self.parser) {
parsers.free(self.parser);
self.parser = null;
}
});
}
util.inherits(Client, net.Stream);
socket.on('end', function() {
self.emit('end');
});
// XXX free the parser?
exports.Client = Client;
if (self.listeners('upgrade').length) {
// Emit 'upgrade' on the Agent.
self.emit('upgrade', res, socket, upgradeHead);
} else {
// Got upgrade header, but have no handler.
socket.destroy();
}
}
};
socket.onend = function() {
self.emit('end'); // mostly for the shim.
parser.finish();
socket.destroy();
};
exports.createClient = function(port, host, https, credentials) {
var c = new Client();
c.port = port;
c.host = host;
c.https = https;
c.credentials = credentials;
return c;
};
// When the socket closes remove it from the list of available sockets.
socket.on('close', function() {
// This is really hacky: What if someone issues a request, the server
// accepts, but then terminates the connection. There is no parse error,
// there is no socket-level error. How does the user get informed?
// We check to see if the socket has a request, if so if it has a
// response (meaning that it emitted a 'response' event). If the socket
// has a request but no response and it never emitted an error event:
// THEN we need to trigger it manually.
// There must be a better way to do this.
if (socket._httpMessage &&
!socket._httpMessage.res &&
!socket._httpMessage._hadError) {
socket._httpMessage.emit('error', new Error('socket hang up'));
}
self._removeSocket(socket);
// unref the parser for easy gc
parsers.free(parser);
});
parser.onIncoming = function(res, shouldKeepAlive) {
debug('AGENT incoming response!');
Client.prototype._initParser = function() {
var self = this;
if (!self.parser) self.parser = parsers.alloc();
self.parser.reinitialize('response');
self.parser.socket = self;
self.parser.onIncoming = function(res) {
debug('CLIENT incoming response!');
var req = socket._httpMessage;
assert(req);
var req = self._outgoing[0];
req.res = res;
// Responses to HEAD requests are AWFUL. Ask Ryan.
// A major oversight in HTTP. Hence this nastiness.
var isHeadResponse = req.method == 'HEAD';
debug('CLIENT isHeadResponse ' + isHeadResponse);
debug('AGENT isHeadResponse ' + isHeadResponse);
if (res.statusCode == 100) {
// restart the parser, as this is a continue message.
@ -982,17 +1067,14 @@ Client.prototype._initParser = function() {
}
res.addListener('end', function() {
debug('CLIENT request complete disconnecting. state = ' + self._state);
debug('AGENT request complete disconnecting.');
// For the moment we reconnect for every request. FIXME!
// All that should be required for keep-alive is to not reconnect,
// but outgoingFlush instead.
if (req.shouldKeepAlive) {
outgoingFlush(self);
self._outgoing.shift();
outgoingFlush(self);
} else {
self.end();
}
if (!req.shouldKeepAlive) socket.end();
req.detachSocket(socket);
self._cycle();
});
req.emit('response', res);
@ -1002,54 +1084,170 @@ Client.prototype._initParser = function() {
};
// This is called each time a request has been pushed completely to the
// socket. The message that was sent is still sitting at client._outgoing[0]
// it is our responsibility to shift it off.
//
// We have to be careful when it we shift it because once we do any writes
// to other requests will be flushed directly to the socket.
//
// At the moment we're implement a client which connects and disconnects on
// each request/response cycle so we cannot shift off the request from
// client._outgoing until we're completely disconnected after the response
// comes back.
Client.prototype._onOutgoingSent = function(message) {
// We've just finished a message. We don't end/shutdown the connection here
// because HTTP servers typically cannot handle half-closed connections
// (Node servers can).
//
// Instead, we just check if the connection is closed, and if so
// reconnect if we have pending messages.
if (this._outgoing.length) {
debug('CLIENT request flush. ensure connection. state = ' + this._state);
this._ensureConnection();
// Sub-classes can overwrite this method with e.g. something that supplies
// TLS streams.
Agent.prototype._getConnection = function(host, port, cb) {
debug("Agent connected!");
var c = net.createConnection(port, host);
c.on('connect', cb);
return c;
};
// This method attempts to shuffle items along the queue into one of the
// waiting sockets. If a waiting socket cannot be found, it will
// start the process of establishing one.
Agent.prototype._cycle = function() {
debug("Agent _cycle");
var first = this.queue[0];
if (!first) return;
// First try to find an available socket.
for (var i = 0; i < this.sockets.length; i++) {
var socket = this.sockets[i];
// If the socket doesn't already have a message it's sending out
// and the socket is available for writing...
if (!socket._httpMessage && (socket.writable && socket.readable)) {
debug("Agent found socket, shift");
// We found an available connection!
this.queue.shift(); // remove first from queue.
first.assignSocket(socket);
return;
}
}
// Otherwise see if we should be starting a new connection to handle
// this.
if (this.sockets.length < this.maxSockets) {
this._establishNewConnection();
}
// All sockets are filled and all sockets are busy.
};
// process-wide hash of agents.
// keys: "host:port" string
// values: instance of Agent
// That is, one agent remote host.
// TODO currently we never remove agents from this hash. This is a small
// memory leak. Have a 2 second timeout after a agent's sockets are to try
// to remove it?
var agents = {}
function getAgent(host, port) {
var id = host + ':' + port;
var agent = agents[id];
if (!agent) {
agent = agents[id] = new Agent(host, port);
}
return agent;
}
exports.request = function(options, cb) {
var agent = getAgent(options.host, options.port);
var req = agent.appendMessage(options);
if (cb) req.once('response', cb);
return req;
};
exports.get = function(options, cb) {
options.method = 'GET';
var req = exports.request(options, cb);
req.end();
return req;
};
// Shims to old interface.
function Client(port, host) {
var self = this;
this.port = port;
this.host = host;
this.agent = getAgent(this.host, this.port);
// proxy connect events upwards;
this.agent.on('connect', function() {
self.emit('connect');
});
this.agent.on('end', function() {
self.emit('end');
});
// proxy upgrade events upwards;
this.agent.on('upgrade', function (res, socket, upgradeHead) {
if (self.listeners('upgrade').length) {
self.emit('upgrade', res, socket, upgradeHead);
} else {
socket.destroy();
}
});
}
util.inherits(Client, EventEmitter);
// This method is used in a few tests to force the connections closed.
// Again - just a shim so as not to break code. Not really important.
Client.prototype.end = function() {
for (var i = 0; i < this.agent.sockets.length; i++) {
var socket = this.agent.sockets[i];
if (!socket._httpMessage && socket.writable) socket.end();
}
};
Client.prototype._ensureConnection = function() {
if (this._state == 'disconnected') {
debug('CLIENT reconnecting state = ' + this._state);
this.connect(this.port, this.host);
this._state = 'connecting';
Client.prototype.destroy = function(e) {
for (var i = 0; i < this.agent.sockets.length; i++) {
var socket = this.agent.sockets[i];
socket.destroy(e);
}
};
Client.prototype.request = function(method, url, headers) {
if (typeof(url) != 'string') {
Client.prototype.request = function(method, path, headers) {
if (typeof(path) != 'string') {
// assume method was omitted, shift arguments
headers = url;
url = method;
headers = path;
path = method;
method = 'GET';
}
var req = new ClientRequest(this, method, url, headers);
this._outgoing.push(req);
this._ensureConnection();
var options = {
method: method,
path: path,
headers: headers,
port: this.port,
host: this.host
};
var self = this;
var req = exports.request(options);
// proxy error events from req to Client
req.on('error', function(err) {
self.emit('error', err);
});
return req;
};
exports.createClient = function(port, host) {
return new Client(port, host);
};
exports.cat = function(url, encoding_, headers_) {
var encoding = 'utf8',
headers = {},
@ -1076,49 +1274,26 @@ exports.cat = function(url, encoding_, headers_) {
var url = require('url').parse(url);
var hasHost = false;
if (Array.isArray(headers)) {
for (var i = 0, l = headers.length; i < l; i++) {
if (headers[i][0].toLowerCase() === 'host') {
hasHost = true;
break;
}
}
} else if (typeof headers === 'Object') {
var keys = Object.keys(headers);
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
if (key.toLowerCase() == 'host') {
hasHost = true;
break;
}
}
}
if (!hasHost) headers['Host'] = url.hostname;
var options = {
method: 'GET',
port: url.port || 80,
host: url.hostname,
headers: headers,
path: (url.pathname || '/') + (url.search || '') + (url.hash || '')
};
var content = '';
var client = exports.createClient(url.port || 80, url.hostname);
var req = client.request((url.pathname || '/') +
(url.search || '') +
(url.hash || ''),
headers);
if (url.protocol == 'https:') {
client.https = true;
}
var callbackSent = false;
req.addListener('response', function(res) {
var req = exports.request(options, function(res) {
if (res.statusCode < 200 || res.statusCode >= 300) {
if (callback && !callbackSent) {
callback(res.statusCode);
callbackSent = true;
}
client.end();
return;
}
res.setEncoding(encoding);
res.addListener('data', function(chunk) { content += chunk; });
res.addListener('end', function() {
@ -1128,19 +1303,13 @@ exports.cat = function(url, encoding_, headers_) {
}
});
});
req.end();
client.addListener('error', function(err) {
if (callback && !callbackSent) {
callback(err);
callbackSent = true;
}
});
client.addListener('close', function() {
req.on('error', function(err) {
if (callback && !callbackSent) {
callback(new Error('Connection closed unexpectedly'));
callback(err);
callbackSent = true;
}
});
req.end();
};

1
src/node_http_parser.cc

@ -229,7 +229,6 @@ class Parser : public ObjectWrap {
}
parser->Wrap(args.This());
assert(!current_buffer);
return args.This();
}

6
test/pummel/test-http-client-reconnect-bug.js

@ -11,11 +11,12 @@ var eofCount = 0;
var server = net.createServer(function(socket) {
socket.end();
});
server.on('listening', function() {
var client = http.createClient(common.PORT);
client.addListener('error', function(err) {
console.log('ERROR! ' + (err.stack || err));
console.log('ERROR! ' + err.message);
errorCount++;
});
@ -30,6 +31,7 @@ server.on('listening', function() {
console.log('STATUS: ' + response.statusCode);
});
});
server.listen(common.PORT);
setTimeout(function() {
@ -38,6 +40,6 @@ setTimeout(function() {
process.addListener('exit', function() {
assert.equal(0, errorCount);
assert.equal(1, errorCount);
assert.equal(1, eofCount);
});

6
test/pummel/test-http-upload-timeout.js

@ -14,18 +14,20 @@ server.on('request', function(req, res) {
});
req.on('end', function() {
connections--;
req.socket.end();
res.writeHead(200);
res.end("done\n");
if (connections == 0) {
server.close();
}
});
});
server.listen(common.PORT, '127.0.0.1', function() {
for (var i = 0; i < 10; i++) {
connections++;
setTimeout(function() {
var client = http.createClient(common.PORT, '127.0.0.1'),
var client = http.createClient(common.PORT),
request = client.request('POST', '/');
function ping() {

12
test/simple/test-http-set-timeout.js

@ -7,6 +7,7 @@ var server = http.createServer(function(req, res) {
req.connection.setTimeout(500);
req.connection.addListener('timeout', function() {
req.connection.destroy();
common.debug('TIMEOUT');
server.close();
});
@ -19,9 +20,10 @@ server.listen(common.PORT, function() {
throw new Error('Timeout was not sucessful');
}, 2000);
http.cat('http://localhost:' + common.PORT + '/', 'utf8',
function(err, content) {
clearTimeout(errorTimer);
console.log('HTTP REQUEST COMPLETE (this is good)');
});
var url = 'http://localhost:' + common.PORT + '/';
http.cat(url, 'utf8', function(err, content) {
clearTimeout(errorTimer);
console.log('HTTP REQUEST COMPLETE (this is good)');
});
});

3
test/simple/test-http-upgrade-client2.js

@ -21,11 +21,13 @@ server.listen(common.PORT, function() {
var client = http.createClient(common.PORT);
function upgradeRequest(fn) {
console.log("req");
var header = { 'Connection': 'Upgrade', 'Upgrade': 'Test' };
var request = client.request('GET', '/', header);
var wasUpgrade = false;
function onUpgrade(res, socket, head) {
console.log("client upgraded");
wasUpgrade = true;
client.removeListener('upgrade', onUpgrade);
@ -34,6 +36,7 @@ server.listen(common.PORT, function() {
client.on('upgrade', onUpgrade);
function onEnd() {
console.log("client end");
client.removeListener('end', onEnd);
if (!wasUpgrade) {
throw new Error('hasn\'t received upgrade event');

Loading…
Cancel
Save