Use old http.Client
authorRyan Dahl <ry@tinyclouds.org>
Tue, 25 Jan 2011 06:27:31 +0000 (22:27 -0800)
committerRyan Dahl <ry@tinyclouds.org>
Tue, 25 Jan 2011 20:13:20 +0000 (12:13 -0800)
This is meant as a path for upgrading to the new http.request() API.
http.Client will be disappearing in the future.

lib/http.js

index 6dccd97..a489705 100644 (file)
@@ -1204,87 +1204,219 @@ exports.get = function(options, cb) {
 };
 
 
-// Shims to old interface.
 
-function Client(port, host) {
+// Legacy Interface:
+
+
+
+function Client() {
+  if (!(this instanceof Client)) return new Client();
+  net.Stream.call(this, { allowHalfOpen: true });
   var self = this;
 
-  this.port = port;
-  this.host = host;
-  this.agent = getAgent(this.host, this.port);
+  // Possible states:
+  // - disconnected
+  // - connecting
+  // - connected
+  this._state = 'disconnected';
 
-  // proxy connect events upwards;
-  this.agent.on('connect', function() {
-    self.emit('connect');
-  });
+  httpSocketSetup(self);
+  this._outgoing = [];
+
+  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;
+      self._state = 'upgraded';
+
+      var res = self.parser.incoming;
+
+      if (self._httpMessage) {
+        self._httpMessage.detachSocket(self);
+      }
+
+      var upgradeHead = d.slice(start + bytesParsed + 1, end);
 
-  this.agent.on('end', function() {
-    self.emit('end');
+      if (self.listeners('upgrade').length) {
+        self.emit('upgrade', res, self, upgradeHead);
+      } else {
+        self.destroy();
+      }
+    }
+  };
+
+  self.addListener('connect', function() {
+    debug('CLIENT connected');
+
+    self.ondata = onData;
+    self.onend = onEnd;
+
+    self._state = 'connected';
+
+    self._initParser();
+
+    self._cycle();
   });
 
-  // 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();
+  function onEnd() {
+    if (self.parser) self.parser.finish();
+    debug('CLIENT got end closing. state = ' + self._state);
+    self.end();
+  };
+
+  self.addListener('close', function(e) {
+    self._state = 'disconnected';
+    self._upgraded = false;
+
+    // Free the parser.
+    if (self.parser) {
+      parsers.free(self.parser);
+      self.parser = null;
     }
+
+    if (e) return;
+
+    // If we have an http message, then drop it
+    var req = self._httpMessage;
+    if (req && !req.res) {
+      req.detachSocket(self);
+      self.emit('error', new Error('socket hang up'));
+    }
+
+    debug('CLIENT onClose. state = ' + self._state);
+    self._cycle();
   });
 }
-util.inherits(Client, EventEmitter);
+util.inherits(Client, net.Stream);
+
+
+exports.Client = Client;
+
+
+exports.createClient = function(port, host) {
+  var c = new Client();
+  c.port = port;
+  c.host = host;
+  return c;
+};
+
+
+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!');
+
+    assert(self._httpMessage);
+    var req = self._httpMessage;
+
+    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);
+
+    if (res.statusCode == 100) {
+      // restart the parser, as this is a continue message.
+      req.emit('continue');
+      return true;
+    }
+
+    if (req.shouldKeepAlive && res.headers.connection === 'close') {
+      req.shouldKeepAlive = false;
+    }
+
+    res.addListener('end', function() {
+      debug('CLIENT response complete disconnecting. state = ' + self._state);
+
+      if (!req.shouldKeepAlive) {
+        self.end();
+      }
+
+      req.detachSocket(self);
+      assert(!self._httpMessage);
+      self._cycle();
+    });
+
+    req.emit('response', res);
+
+    return isHeadResponse;
+  };
+};
+
 
+Client.prototype._cycle = function() {
+  debug("Client _cycle");
+  if (this._upgraded) return;
+
+  switch (this._state) {
+    case 'connecting':
+      break;
 
-// 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();
+    case 'connected':
+      if (this.writable && this.readable) {
+        debug("Client _cycle shift()");
+        if (this._httpMessage) {
+          this._httpMessage._flush();
+        } else {
+          var req = this._outgoing.shift();
+          if (req) req.assignSocket(this);
+        }
+      }
+      break;
+
+    case 'disconnected':
+      if (this._httpMessage || this._outgoing.length) {
+        this._ensureConnection();
+      }
+      break;
   }
 };
 
 
-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._ensureConnection = function() {
+  if (this._state == 'disconnected') {
+    debug('CLIENT reconnecting state = ' + this._state);
+    this.connect(this.port, this.host);
+    this._state = 'connecting';
   }
 };
 
 
-Client.prototype.request = function(method, path, headers) {
-  if (typeof(path) != 'string') {
+Client.prototype.request = function(method, url, headers) {
+  if (typeof(url) != 'string') {
     // assume method was omitted, shift arguments
-    headers = path;
-    path = method;
+    headers = url;
+    url = method;
     method = 'GET';
   }
 
+  var self = this;
+
   var options = {
-    method: method,
-    path: path,
-    headers: headers,
-    port: this.port,
-    host: this.host
+    method: method || 'GET',
+    path: url || '/',
+    headers: headers
   };
 
-  var self = this;
-  var req = exports.request(options);
-
-  // proxy error events from req to Client
-  req.on('error', function(err) {
-    self.emit('error', err);
-  });
+  var req = new ClientRequest(options);
+  this._outgoing.push(req);
+  this._cycle();
 
   return req;
 };
 
 
-exports.createClient = function(port, host) {
-  return new Client(port, host);
-};
-
-
 exports.cat = function(url, encoding_, headers_) {
   var encoding = 'utf8',
       headers = {},
@@ -1311,26 +1443,49 @@ exports.cat = function(url, encoding_, headers_) {
 
   var url = require('url').parse(url);
 
-  var options = {
-    method: 'GET',
-    port: url.port || 80,
-    host: url.hostname,
-    headers: headers,
-    path: (url.pathname || '/') + (url.search || '') + (url.hash || '')
-  };
+  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 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;
 
-  var req = exports.request(options, function(res) {
+  req.addListener('response', 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() {
@@ -1340,13 +1495,19 @@ exports.cat = function(url, encoding_, headers_) {
       }
     });
   });
-  req.end();
 
-
-  req.on('error', function(err) {
+  client.addListener('error', function(err) {
     if (callback && !callbackSent) {
       callback(err);
       callbackSent = true;
     }
   });
+
+  client.addListener('close', function() {
+    if (callback && !callbackSent) {
+      callback(new Error('Connection closed unexpectedly'));
+      callbackSent = true;
+    }
+  });
+  req.end();
 };