diff --git a/doc/api/http.markdown b/doc/api/http.markdown index c61c544773..e818582a9f 100644 --- a/doc/api/http.markdown +++ b/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 . +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. diff --git a/lib/http.js b/lib/http.js index 960d28aaa3..d9633024d7 100644 --- a/lib/http.js +++ b/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(); }; diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index 31e0ae5b60..7390a71127 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -229,7 +229,6 @@ class Parser : public ObjectWrap { } parser->Wrap(args.This()); - assert(!current_buffer); return args.This(); } diff --git a/test/pummel/test-http-client-reconnect-bug.js b/test/pummel/test-http-client-reconnect-bug.js index 3b102fc0f6..b104f4dd46 100644 --- a/test/pummel/test-http-client-reconnect-bug.js +++ b/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); }); diff --git a/test/pummel/test-http-upload-timeout.js b/test/pummel/test-http-upload-timeout.js index 4fb4980019..06b8e74496 100644 --- a/test/pummel/test-http-upload-timeout.js +++ b/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() { diff --git a/test/simple/test-http-set-timeout.js b/test/simple/test-http-set-timeout.js index 1ae3229cae..d9b54c2f94 100644 --- a/test/simple/test-http-set-timeout.js +++ b/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)'); + }); }); diff --git a/test/simple/test-http-upgrade-client2.js b/test/simple/test-http-upgrade-client2.js index 14a2fa1798..bdd585a13c 100644 --- a/test/simple/test-http-upgrade-client2.js +++ b/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');