Bug 469228 - Part1: Support keep-alive, r=Waldo
☠☠ backed out by 4f45c9de5a9e ☠ ☠
authorKershaw Chang <kechang@mozilla.com>
Wed, 10 May 2017 03:48:00 +0200
changeset 405673 2e22c0308a7ea5f19d4616d408a65cdf39292c4c
parent 405672 7cd208304d1fb4a58908d707d991eb5b10d53f7b
child 405674 8d46046a7367540c311a9abbfb4fcf3f868bb824
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersWaldo
bugs469228
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 469228 - Part1: Support keep-alive, r=Waldo
netwerk/test/httpserver/httpd.js
netwerk/test/httpserver/nsIHttpServer.idl
--- a/netwerk/test/httpserver/httpd.js
+++ b/netwerk/test/httpserver/httpd.js
@@ -156,16 +156,19 @@ const HIDDEN_CHAR = "^";
  * The file name suffix indicating the file containing overridden headers for
  * a requested file.
  */
 const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR;
 
 /** Type used to denote SJS scripts for CGI-like functionality. */
 const SJS_TYPE = "sjs";
 
+/** The number of seconds to keep idle persistent connections alive. */
+const DEFAULT_KEEP_ALIVE_TIMEOUT = 2 * 60;
+
 /** Base for relative timestamps produced by dumpn(). */
 var firstStamp = 0;
 
 /** dump(str) with a trailing "\n" -- only outputs if DEBUG. */
 function dumpn(str)
 {
   if (DEBUG)
   {
@@ -233,16 +236,18 @@ const FileInputStream = CC("@mozilla.org
 const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1",
                                 "nsIConverterInputStream",
                                 "init");
 const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1",
                                "nsIWritablePropertyBag2");
 const SupportsString = CC("@mozilla.org/supports-string;1",
                           "nsISupportsString");
 
+const Timer = CC("@mozilla.org/timer;1", "nsITimer");
+
 /* These two are non-const only so a test can overwrite them. */
 var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
                            "nsIBinaryInputStream",
                            "setInputStream");
 var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
                             "nsIBinaryOutputStream",
                             "setOutputStream");
 
@@ -386,16 +391,23 @@ function nsHttpServer()
    */
   this._connectionGen = 0;
 
   /**
    * Hash of all open connections, indexed by connection number at time of
    * creation.
    */
   this._connections = {};
+
+  /**
+   * Flag for keep-alive behavior, true for default behavior, false for
+   * closing connections after the first response.
+   * Set by nsIHttpServer.keepAlive attribute.
+   */
+  this._keepAliveEnabled = true;
 }
 nsHttpServer.prototype =
 {
   classID: Components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"),
 
   // NSISERVERSOCKETLISTENER
 
   /**
@@ -430,24 +442,17 @@ nsHttpServer.prototype =
     }
 
     var connectionNumber = ++this._connectionGen;
 
     try
     {
       var conn = new Connection(input, output, this, socket.port, trans.port,
                                 connectionNumber);
-      var reader = new RequestReader(conn);
-
-      // XXX add request timeout functionality here!
-
-      // Note: must use main thread here, or we might get a GC that will cause
-      //       threadsafety assertions.  We really need to fix XPConnect so that
-      //       you can actually do things in multi-threaded JS.  :-(
-      input.asyncWait(reader, 0, 0, gThreadManager.mainThread);
+      conn.read();
     }
     catch (e)
     {
       // Assume this connection can't be salvaged and bail on it completely;
       // don't attempt to close it so that we can assert that any connection
       // being closed is in this._connections.
       dumpn("*** error in initial request-processing stages: " + e);
       trans.close(Cr.NS_BINDING_ABORTED);
@@ -467,17 +472,17 @@ nsHttpServer.prototype =
    *   the reason the socket stopped listening (NS_BINDING_ABORTED if the server
    *   was stopped using nsIHttpServer.stop)
    * @see nsIServerSocketListener.onStopListening
    */
   onStopListening: function(socket, status)
   {
     dumpn(">>> shutting down server on port " + socket.port);
     for (var n in this._connections) {
-      if (!this._connections[n]._requestStarted) {
+      if (!this._connections[n]._requestStarted || this._connections[n].isIdle()) {
         this._connections[n].close();
       }
     }
     this._socketClosed = true;
     if (this._hasOpenConnections()) {
       dumpn("*** open connections!!!");
     }
     if (!this._hasOpenConnections())
@@ -738,16 +743,28 @@ nsHttpServer.prototype =
   {
     return this._handler._setObjectState(k, v);
   },
 
   get wrappedJSObject() {
     return this;
   },
 
+  //
+  // see nsIHttpServer.keepAliveEnabled
+  //
+  get keepAliveEnabled()
+  {
+    return this._keepAliveEnabled;
+  },
+
+  set keepAliveEnabled(doKeepAlive)
+  {
+    this._keepAliveEnabled = doKeepAlive;
+  },
 
   // NSISUPPORTS
 
   //
   // see nsISupports.QueryInterface
   //
   QueryInterface: function(iid)
   {
@@ -837,16 +854,32 @@ nsHttpServer.prototype =
       this._notifyStopped();
     // Bug 508125: Add a GC here else we'll use gigabytes of memory running
     // mochitests. We can't rely on xpcshell doing an automated GC, as that
     // would interfere with testing GC stuff...
     Components.utils.forceGC();
   },
 
   /**
+   * Inform the server that the connection is currently in an idle state and
+   * may be closed when the server goes down.
+   */
+  _connectionIdle: function(connection)
+  {
+    // If the server is down, close any now-idle connections.
+    if (this._socketClosed)
+    {
+      connection.close();
+      return;
+    }
+
+    connection.persist();
+   },
+
+  /**
    * Requests that the server be shut down when possible.
    */
   _requestQuit: function()
   {
     dumpn(">>> requesting a quit");
     dumpStack();
     this._doQuit = true;
   }
@@ -1182,44 +1215,133 @@ function Connection(input, output, serve
    *  close and the socket being forced closed on shutdown.
    */
   this._closed = false;
 
   /** State variable for debugging. */
   this._processed = false;
 
   /** whether or not 1st line of request has been received */
-  this._requestStarted = false; 
+  this._requestStarted = false;
+
+  /**
+   * RequestReader may cache the port number here. This is needed because when
+   * going through ssltunnel, we update the Request-URL only for the first
+   * request, though we get knowledge of the actual target server port number.
+   * Subsequent requests are not updated that way and are missing the port number
+   * that may lead to mismatch of the target virtual server identity.
+   *
+   * Default to standard HTTP port 80 to accept common hosts (the port number is
+   * not sent by clients when it is the default port number for a scheme, nor
+   * the scheme is sent by default).
+   *
+   * See also RequestReader._validateRequest method.
+   */
+  this._currentIncomingPort = 80;
+
+  /**
+   * The current keep-alive timeout duration, in seconds.
+   * Individual requests/responses can modify this value through the Keep-Alive header.
+   */
+  this._keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT;
+
+  /**
+   * Start the initial timeout. If no data is read from this connection within
+   * the keep alive timeout, then close this connection.
+   */
+  this.persist();
 }
 Connection.prototype =
 {
+  /**
+   * Wait for an incoming data on the connection and
+   * expect to get a new full request.
+   */
+  read: function()
+  {
+    dumpn("*** read on connection " + this);
+    this._processed = false;
+    this.request = null;
+
+    this.server._connectionIdle(this);
+
+    var reader = new RequestReader(this);
+
+    // XXX add request timeout functionality here!
+
+    try
+    {
+      this.input.asyncWait(reader, 0, 0, gThreadManager.mainThread);
+    }
+    catch (e)
+    {
+      this.close();
+    }
+  },
+
   /** Closes this connection's input/output streams. */
   close: function()
   {
     if (this._closed)
         return;
 
     dumpn("*** closing connection " + this.number +
           " on port " + this._outgoingPort);
 
+    if (this._idleTimer)
+    {
+      this._idleTimer.cancel();
+      this._idleTimer = null;
+    }
+
     this.input.close();
     this.output.close();
     this._closed = true;
 
     var server = this.server;
     server._connectionClosed(this);
 
     // If an error triggered a server shutdown, act on it now
     if (server._doQuit)
       server.stop(function() { /* not like we can do anything better */ });
   },
 
+  /** Let the connection be persistent for the keep-alive time */
+  persist: function()
+  {
+    // This method is called every time the connection gets to an idle state,
+    // i.e. has sent all queued responses out and doesn't process a request now.
+    var idleTimer = this._idleTimer;
+    if (idleTimer)
+      this._idleTimer.cancel();
+    else
+      this._idleTimer = idleTimer = new Timer();
+
+    dumpn("*** persisting idle connection " + this +
+          " for " + this._keepAliveTimeout + " seconds");
+
+    var connection = this;
+    idleTimer.initWithCallback(function()
+    {
+      // We might get to an active state before the timeout occurred, then
+      // ignore it. The timer will be rescheduled when the connection returns
+      // to the idle state.  This is simpler and less error-prone then canceling
+      // the timer whenever the connection becomes active again.
+      if (!connection.isIdle())
+        return;
+
+      dumpn("*** closing idle connection " + connection);
+      connection.close();
+    }, this._keepAliveTimeout * 1000, Ci.nsITimer.TYPE_ONE_SHOT);
+  },
+
   /**
    * Initiates processing of this connection, using the data in the given
-   * request.
+   * request. Called by RequestReader._handleResponse after the request has
+   * been completely read from the socket.
    *
    * @param request : Request
    *   the request which should be processed
    */
   process: function(request)
   {
     NS_ASSERT(!this._closed && !this._processed);
 
@@ -1243,16 +1365,26 @@ Connection.prototype =
   {
     NS_ASSERT(!this._closed && !this._processed);
 
     this._processed = true;
     this.request = request;
     this.server._handler.handleError(code, this);
   },
 
+  /**
+   * Returns true iff this connection is not closed and is idle.
+   * A connection is idle when all requests received on it have triggered responses,
+   * and those responses have been completely sent.
+   */
+  isIdle: function()
+  {
+    return this.request === null && !this._closed;
+  },
+
   /** Converts this to a string for debugging purposes. */
   toString: function()
   {
     return "<Connection(" + this.number +
            (this.request ? ", " + this.request.path : "") +"): " +
            (this._closed ? "closed" : "open") + ">";
   },
 
@@ -1358,17 +1490,17 @@ RequestReader.prototype =
       return;
 
     try
     {
       data.appendBytes(readBytes(input, input.available()));
     }
     catch (e)
     {
-      if (streamClosed(e))
+      if (streamClosed(e) && !this._connection._closed)
       {
         dumpn("*** WARNING: unexpected error when reading from socket; will " +
               "be treated as if the input stream had been closed");
         dumpn("*** WARNING: actual error was: " + e);
       }
 
       // We've lost a race -- input has been closed, but we're still expecting
       // to read more data.  available() will throw in this case, and since
@@ -1596,17 +1728,20 @@ RequestReader.prototype =
           throw HTTP_400;
         }
 
         // If we're not given a port, we're stuck, because we don't know what
         // scheme to use to look up the correct port here, in general.  Since
         // the HTTPS case requires a tunnel/proxy and thus requires that the
         // requested URI be absolute (and thus contain the necessary
         // information), let's assume HTTP will prevail and use that.
-        port = +port || 80;
+        // _connection._currentIncomingPort defaults to 80 (HTTP) but can be
+        // overwritten by the first tunneled request with a full specified URL.
+        // See also RequestReader.prototype._parseRequestLine.
+        port = +port || this._connection._currentIncomingPort;
 
         var scheme = identity.getScheme(host, port);
         if (!scheme)
         {
           dumpn("*** unrecognized hostname (" + hostPort + ") in Host " +
                 "header, 400 time");
           throw HTTP_400;
         }
@@ -1783,16 +1918,20 @@ RequestReader.prototype =
         throw HTTP_400;
       }
 
       if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/")
       {
         dumpn("*** serverIdentity unknown or path does not start with '/'");
         throw HTTP_400;
       }
+
+      // Remember the port number we have determined. Subsequent requests
+      // might not contain this information.
+      this._connection._currentIncomingPort = port;
     }
 
     var splitter = fullPath.indexOf("?");
     if (splitter < 0)
     {
       // _queryString already set in ctor
       metadata._path = fullPath;
     }
@@ -3534,16 +3673,27 @@ function isCTL(code)
  *   the connection over which this response is to be written
  */
 function Response(connection)
 {
   /** The connection over which this response will be written. */
   this._connection = connection;
 
   /**
+   * If true, close the connection at after the response has been sent out.
+   * Can be set to true whenever before end() method has been called.
+   * If no connection header was present then default to "keep-alive" for
+   * HTTP/1.1 and "close" for HTTP/1.0.
+   */
+  var req = connection.request;
+  this._closeConnection = !connection.server._keepAliveEnabled ||
+                          (req.hasHeader("Connection")
+                          ? req.getHeader("Connection").split(",").includes("close")
+                          : !req._httpVersion.atLeast(nsHttpVersion.HTTP_1_1));
+  /**
    * The HTTP version of this response; defaults to 1.1 if not set by the
    * handler.
    */
   this._httpVersion = nsHttpVersion.HTTP_1_1;
 
   /**
    * The HTTP code of this response; defaults to 200.
    */
@@ -3594,16 +3744,22 @@ function Response(connection)
   /**
    * True if this response has been designated as being processed
    * asynchronously rather than for the duration of a single call to
    * nsIHttpRequestHandler.handle.
    */
   this._processAsync = false;
 
   /**
+   * Flag indicating use of the chunked encoding to send the asynchronously
+   * generated content.
+   */
+  this._chunked = false;
+
+  /**
    * True iff finish() has been called on this, signaling that no more changes
    * to this may be made.
    */
   this._finished = false;
 
   /**
    * True iff powerSeized() has been called on this, signaling that this
    * response is to be handled manually by the response handler (which may then
@@ -3774,37 +3930,52 @@ Response.prototype =
     {
       var input = new BinaryInputStream(this._bodyInputStream);
       var avail;
       while ((avail = input.available()) > 0)
         input.readByteArray(avail);
     }
 
     this._powerSeized = true;
+    this._closeConnection = true;
     if (this._bodyOutputStream)
       this._startAsyncProcessor();
   },
 
   //
   // see nsIHttpResponse.finish
   //
   finish: function()
   {
     if (!this._processAsync && !this._powerSeized)
       throw Cr.NS_ERROR_UNEXPECTED;
     if (this._finished)
       return;
 
     dumpn("*** finishing connection " + this._connection.number);
     this._startAsyncProcessor(); // in case bodyOutputStream was never accessed
+
+    // If we are using chunked encoding then ensure body streams are present
+    // so that WriteTrhoughCopier will send an EOF chunk through.
+    if (this._chunked)
+      this.bodyOutputStream;
+
     if (this._bodyOutputStream)
       this._bodyOutputStream.close();
     this._finished = true;
   },
 
+  //
+  // see nsIHttpResponse.closeConnection
+  //
+  closeConnection: function()
+  {
+    dumpn("*** disable keep-alive for connection " + this._connection.number);
+    this._closeConnection = true;
+  },
 
   // NSISUPPORTS
 
   //
   // see nsISupports.QueryInterface
   //
   QueryInterface: function(iid)
   {
@@ -3921,16 +4092,19 @@ Response.prototype =
    * @param e : Error
    *   the exception which precipitated this abort, or null if no such exception
    *   was generated
    */
   abort: function(e)
   {
     dumpn("*** abort(<" + e + ">)");
 
+    // Close the connection in case of any error.
+    this._closeConnection = true;
+
     // This response will be ended by the processor if one was created.
     var copier = this._asyncCopier;
     if (copier)
     {
       // We dispatch asynchronously here so that any pending writes of data to
       // the connection will be deterministically written.  This makes it easier
       // to specify exact behavior, and it makes observable behavior more
       // predictable for clients.  Note that the correctness of this depends on
@@ -3960,17 +4134,21 @@ Response.prototype =
   /**
    * Closes this response's network connection, marks the response as finished,
    * and notifies the server handler that the request is done being processed.
    */
   end: function()
   {
     NS_ASSERT(!this._ended, "ending this response twice?!?!");
 
-    this._connection.close();
+    if (this._closeConnection)
+      this._connection.close();
+    else
+      this._connection.read(); /* restart reading this keep-alive connection */
+
     if (this._bodyOutputStream)
       this._bodyOutputStream.close();
 
     this._finished = true;
     this._ended = true;
   },
 
   // PRIVATE IMPLEMENTATION
@@ -4023,17 +4201,53 @@ Response.prototype =
     // request-line
     var statusLine = "HTTP/" + this.httpVersion + " " +
                      this.httpCode + " " +
                      this.httpDescription + "\r\n";
 
     // header post-processing
 
     var headers = this._headers;
-    headers.setHeader("Connection", "close", false);
+    if (headers.hasHeader("Connection"))
+    {
+      // If "Connection: close" header had been manually set on the response,
+      // then set our _closeConnection flag, otherwise leave it as is.
+      this._closeConnection = this._closeConnection ||
+        headers.getHeader("Connection").split(",").includes("close");
+    }
+    else
+    {
+      // There is no Connection header set on the response, set it by state
+      // of the _closeConnection flag.
+      var connectionHeaderValue = this._closeConnection
+                                ? "close"
+                                : "keep-alive";
+      headers.setHeader("Connection", connectionHeaderValue, false);
+    }
+
+    if (headers.hasHeader("Keep-Alive"))
+    {
+      // Read the Keep-alive header and set the timeout according it.
+      // Note: This is documented in RFC 2068, section 19.7.1.
+      var keepAliveTimeout = headers.getHeader("Keep-Alive")
+                             .match(/^timeout=(\d+)$/);
+      if (keepAliveTimeout)
+      {
+        var seconds = parseInt(keepAliveTimeout[1], 10);
+        this._connection._keepAliveTimeout = Math.min(seconds, DEFAULT_KEEP_ALIVE_TIMEOUT);
+      }
+    }
+    else if (!this._closeConnection)
+    {
+      // Add the keep alive header.
+      headers.setHeader("Keep-Alive",
+                        "timeout=" + this._connection._keepAliveTimeout,
+                        false);
+    }
+
     headers.setHeader("Server", "httpd.js", false);
     if (!headers.hasHeader("Date"))
       headers.setHeader("Date", toDateString(Date.now()), false);
 
     // Any response not being processed asynchronously must have an associated
     // Content-Length header for reasons of backwards compatibility with the
     // initial server, which fully buffered every response before sending it.
     // Beyond that, however, it's good to do this anyway because otherwise it's
@@ -4045,16 +4259,21 @@ Response.prototype =
 
       var bodyStream = this._bodyInputStream;
       var avail = bodyStream ? bodyStream.available() : 0;
 
       // XXX assumes stream will always report the full amount of data available
       headers.setHeader("Content-Length", "" + avail, false);
     }
 
+    this._chunked = !this._headers.hasHeader("Content-Length");
+    dumpn("*** this._chunked= " + this._chunked);
+
+    if (this._chunked)
+      headers.setHeader("Transfer-Encoding", "chunked", false);
 
     // construct and send response
     dumpn("*** header post-processing completed, sending response head...");
 
     // request-line
     var preambleData = [statusLine];
 
     // headers
@@ -4110,17 +4329,17 @@ Response.prototype =
 
           throw Cr.NS_ERROR_NO_INTERFACE;
         }
       };
 
     var headerCopier = this._asyncCopier =
       new WriteThroughCopier(responseHeadPipe.inputStream,
                              this._connection.output,
-                             copyObserver, null);
+                             copyObserver, null, false);
 
     responseHeadPipe.outputStream.close();
 
     // Forbid setting any more headers or modifying the request line.
     this._headers = null;
   },
 
   /**
@@ -4173,17 +4392,17 @@ Response.prototype =
 
           throw Cr.NS_ERROR_NO_INTERFACE;
         }
       };
 
     dumpn("*** starting async copier of body data...");
     this._asyncCopier =
       new WriteThroughCopier(this._bodyInputStream, this._connection.output,
-                            copyObserver, null);
+                             copyObserver, null, this._chunked);
   },
 
   /** Ensures that this hasn't been ended. */
   _ensureAlive: function()
   {
     NS_ASSERT(!this._ended, "not handling response lifetime correctly");
   }
 };
@@ -4221,36 +4440,41 @@ function wouldBlock(e)
  * @param source : nsIAsyncInputStream
  *   the stream from which data is to be read
  * @param sink : nsIAsyncOutputStream
  *   the stream to which data is to be copied
  * @param observer : nsIRequestObserver
  *   an observer which will be notified when the copy starts and finishes
  * @param context : nsISupports
  *   context passed to observer when notified of start/stop
+ * @param chunked : boolean
+ *   indicate whether to use chunked encoding
  * @throws NS_ERROR_NULL_POINTER
  *   if source, sink, or observer are null
  */
-function WriteThroughCopier(source, sink, observer, context)
+function WriteThroughCopier(source, sink, observer, context, chunked)
 {
   if (!source || !sink || !observer)
     throw Cr.NS_ERROR_NULL_POINTER;
 
   /** Stream from which data is being read. */
   this._source = source;
 
   /** Stream to which data is being written. */
   this._sink = sink;
 
   /** Observer watching this copy. */
   this._observer = observer;
 
   /** Context for the observer watching this. */
   this._context = context;
 
+  /** Forces use of chunked encoding on the data */
+  this._chunked = chunked;
+
   /**
    * True iff this is currently being canceled (cancel has been called, the
    * callback may not yet have been made).
    */
   this._canceled = false;
 
   /**
    * False until all data has been read from input and written to output, at
@@ -4349,17 +4573,47 @@ WriteThroughCopier.prototype =
 
       bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE);
       dumpn("*** input wanted: " + bytesWanted);
 
       if (bytesWanted > 0)
       {
         var data = input.readByteArray(bytesWanted);
         bytesConsumed = data.length;
-        this._pendingData.push(String.fromCharCode.apply(String, data));
+        var dataStr = String.fromCharCode.apply(String, data);
+        if (this._chunked)
+        {
+          // from RFC2616 section 3.6.1, the chunked transfer coding is defined as:
+          //
+          //   Chunked-Body    = *chunk
+          //                     last-chunk
+          //                     trailer
+          //                     CRLF
+          //   chunk           = chunk-size [ chunk-extension ] CRLF
+          //                     chunk-data CRLF
+          //   chunk-size      = 1*HEX
+          //   last-chunk      = 1*("0") [ chunk-extension ] CRLF
+          //
+          //   chunk-extension = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
+          //   chunk-ext-name  = token
+          //   chunk-ext-val   = token | quoted-string
+          //   chunk-data      = chunk-size(OCTET)
+          //   trailer         = *(entity-header CRLF)
+          //
+          // Apply chunked encoding here
+          data = bytesConsumed.toString(16).toUpperCase()
+               + "\r\n"
+               + dataStr
+               + "\r\n";
+          this._pendingData.push(data);
+        }
+        else
+        {
+          this._pendingData.push(dataStr);
+        }
       }
 
       dumpn("*** " + bytesConsumed + " bytes read");
 
       // Handle the zero-data edge case in the same place as all other edge
       // cases are handled.
       if (bytesWanted === 0)
         throw Cr.NS_BASE_STREAM_CLOSED;
@@ -4617,16 +4871,35 @@ WriteThroughCopier.prototype =
    * @param e : nsresult
    *   the status to be used when closing the input stream
    */
   _doneReadingSource: function(e)
   {
     dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")");
 
     this._finishSource(e);
+
+    if (this._chunked && this._sink !== null)
+    {
+      // Write the final EOF chunk
+      dumpn("*** _doneReadingSource - write EOF chunk");
+      this._chunked = false;
+      this._pendingData.push("0\r\n\r\n");
+      try
+      {
+        this._waitToWriteData();
+        return;
+      }
+      catch (e)
+      {
+        dumpn("!!! unexpected error waiting to write pending data: " + e);
+        throw e;
+      }
+    }
+
     if (this._pendingData.length === 0)
       this._sink = null;
     else
       NS_ASSERT(this._sink !== null, "null output?");
 
     // If we've written out all data read up to this point, then it's time to
     // signal completion.
     if (this._sink === null)
--- a/netwerk/test/httpserver/nsIHttpServer.idl
+++ b/netwerk/test/httpserver/nsIHttpServer.idl
@@ -14,17 +14,17 @@ interface nsIHttpServerStoppedCallback;
 interface nsIHttpRequestHandler;
 interface nsIHttpRequest;
 interface nsIHttpResponse;
 interface nsIHttpServerIdentity;
 
 /**
  * An interface which represents an HTTP server.
  */
-[scriptable, uuid(cea8812e-faa6-4013-9396-f9936cbb74ec)]
+[scriptable, uuid(07a990e9-8c49-4fe5-b794-c2dc9654c0df)]
 interface nsIHttpServer : nsISupports
 {
   /**
    * Starts up this server, listening upon the given port.
    *
    * @param port
    *   the port upon which listening should happen, or -1 if no specific port is
    *   desired
@@ -213,16 +213,26 @@ interface nsIHttpServer : nsISupports
    */
   nsISupports getObjectState(in AString key);
 
   /**
    * Sets the object associated with the given key in this in object-valued
    * saved state.  The value may be null.
    */
   void setObjectState(in AString key, in nsISupports value);
+
+  /**
+   * Configures use of keep-alive connections, wherein the same network
+   * connection may be used to receive and respond to multiple requests.
+   * If true, keep-alive connections will be used when possible. If false,
+   * keep-alive connections will not be used, and responses will include a
+   * "Connection: close" header.
+   * Defaults to true.
+   */
+  attribute boolean keepAliveEnabled;
 };
 
 /**
  * An interface through which a notification of the complete stopping (socket
  * closure, in-flight requests all fully served and responded to) of an HTTP
  * server may be received.
  */
 [scriptable, function, uuid(925a6d33-9937-4c63-abe1-a1c56a986455)]
@@ -471,17 +481,17 @@ interface nsIHttpRequest : nsISupports
    */
   readonly attribute nsIInputStream bodyInputStream;
 };
 
 
 /**
  * Represents an HTTP response, as described in RFC 2616, section 6.
  */
-[scriptable, uuid(1acd16c2-dc59-42fa-9160-4f26c43c1c21)]
+[scriptable, uuid(3116c0dc-dd5f-4864-8ebd-fd5359e0b963)]
 interface nsIHttpResponse : nsISupports
 {
   /**
    * Sets the status line for this.  If this method is never called on this, the
    * status line defaults to "HTTP/", followed by the server's default HTTP
    * version (e.g. "1.1"), followed by " 200 OK".
    *
    * @param httpVersion
@@ -612,9 +622,17 @@ interface nsIHttpResponse : nsISupports
    * signals that all data has been written and that the underlying connection
    * may be closed.  This method may only be called after processAsync() or
    * seizePower() has been called.  This method is idempotent.
    *
    * @throws NS_ERROR_UNEXPECTED
    *   if processAsync() or seizePower() has not already been properly called
    */
   void finish();
+
+  /**
+   * Signals that this response's connection will be closed after the response
+   * has been sent out.
+   * If not called, the default behavior is to reuse the connection for next
+   * HTTP transaction when nsIHttpServer.keepAliveEnabled is true.
+   */
+  void closeConnection();
 };