Bug 237623 - detect broken HTTP1.1 transfers. r=mcmanus,seth
authorDaniel Stenberg <daniel@haxx.se>
Mon, 09 Jun 2014 00:15:00 +0200
changeset 188836 bb7ae1cc7789
parent 188835 589789cf7bf8
child 188837 2707ae2da0eb
push id44923
push usercbook@mozilla.com
push date2014-06-16 07:50 +0000
Treeherderresults
reviewersmcmanus, seth
bugs237623
milestone33.0a1
Bug 237623 - detect broken HTTP1.1 transfers. r=mcmanus,seth
Return error when the protocol layer detects a framing error. More data
was supposed to be delivered than what actually did arrive.

Error code returned for this: NS_ERROR_NET_PARTIAL_TRANSFER

In HTTP1.1 for Content-Length: and chunked-encoding underruns

In http2 and SPDY for framing errors when data has already been received.

imgRequest::OnStopRequest will keep partially loaded images shown but
remove them from cache.
image/src/imgRequest.cpp
image/src/imgRequest.h
js/xpconnect/src/xpc.msg
netwerk/protocol/http/Http2Session.cpp
netwerk/protocol/http/Http2Stream.h
netwerk/protocol/http/SpdySession3.cpp
netwerk/protocol/http/SpdySession31.cpp
netwerk/protocol/http/nsHttpTransaction.cpp
netwerk/test/unit/head_channels.js
netwerk/test/unit/test_chunked_responses.js
netwerk/test/unit/test_content_length_underrun.js
netwerk/test/unit/test_gzipped_206.js
netwerk/test/unit/xpcshell.ini
xpcom/base/ErrorList.h
--- a/image/src/imgRequest.cpp
+++ b/image/src/imgRequest.cpp
@@ -294,16 +294,57 @@ void imgRequest::ContinueCancel(nsresult
   RemoveFromCache();
 
   nsRefPtr<imgStatusTracker> statusTracker = GetStatusTracker();
   if (mRequest && statusTracker->IsLoading()) {
      mRequest->Cancel(aStatus);
   }
 }
 
+class imgRequestMainThreadEvict : public nsRunnable
+{
+public:
+  imgRequestMainThreadEvict(imgRequest *aImgRequest)
+    : mImgRequest(aImgRequest)
+  {
+    MOZ_ASSERT(!NS_IsMainThread(), "Create me off main thread only!");
+    MOZ_ASSERT(aImgRequest);
+  }
+
+  NS_IMETHOD Run()
+  {
+    MOZ_ASSERT(NS_IsMainThread(), "I should be running on the main thread!");
+    mImgRequest->ContinueEvict();
+    return NS_OK;
+  }
+private:
+  nsRefPtr<imgRequest> mImgRequest;
+};
+
+// EvictFromCache() is written to allowed to get called from any thread
+void imgRequest::EvictFromCache()
+{
+  /* The EvictFromCache() method here should only be called by this class. */
+  LOG_SCOPE(GetImgLog(), "imgRequest::EvictFromCache");
+
+  if (NS_IsMainThread()) {
+    ContinueEvict();
+  } else {
+    NS_DispatchToMainThread(new imgRequestMainThreadEvict(this));
+  }
+}
+
+// Helper-method used by EvictFromCache()
+void imgRequest::ContinueEvict()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  RemoveFromCache();
+}
+
 nsresult imgRequest::GetURI(ImageURL **aURI)
 {
   MOZ_ASSERT(aURI);
 
   LOG_FUNC(GetImgLog(), "imgRequest::GetURI");
 
   if (mURI) {
     *aURI = mURI;
@@ -663,38 +704,49 @@ NS_IMETHODIMP imgRequest::OnStopRequest(
     mChannel = nullptr;
   }
 
   bool lastPart = true;
   nsCOMPtr<nsIMultiPartChannel> mpchan(do_QueryInterface(aRequest));
   if (mpchan)
     mpchan->GetIsLastPart(&lastPart);
 
+  bool isPartial = false;
+  if (mImage && (status == NS_ERROR_NET_PARTIAL_TRANSFER)) {
+    isPartial = true;
+    status = NS_OK; // fake happy face
+  }
+
   // Tell the image that it has all of the source data. Note that this can
   // trigger a failure, since the image might be waiting for more non-optional
   // data and this is the point where we break the news that it's not coming.
   if (mImage) {
     nsresult rv = mImage->OnImageDataComplete(aRequest, ctxt, status, lastPart);
 
     // If we got an error in the OnImageDataComplete() call, we don't want to
     // proceed as if nothing bad happened. However, we also want to give
     // precedence to failure status codes from necko, since presumably they're
     // more meaningful.
     if (NS_FAILED(rv) && NS_SUCCEEDED(status))
       status = rv;
   }
 
   // If the request went through, update the cache entry size. Otherwise,
   // cancel the request, which removes us from the cache.
-  if (mImage && NS_SUCCEEDED(status)) {
+  if (mImage && NS_SUCCEEDED(status) && !isPartial) {
     // We update the cache entry size here because this is where we finish
     // loading compressed source data, which is part of our size calculus.
     UpdateCacheEntrySize();
   }
+  else if (isPartial) {
+    // Remove the partial image from the cache.
+    this->EvictFromCache();
+  }
   else {
+    // if the error isn't "just" a partial transfer
     // stops animations, removes from cache
     this->Cancel(status);
   }
 
   if (!mImage) {
     // We have to fire imgStatusTracker::OnStopRequest ourselves because there's
     // no image capable of doing so.
     nsRefPtr<imgStatusTracker> statusTracker = GetStatusTracker();
--- a/image/src/imgRequest.h
+++ b/image/src/imgRequest.h
@@ -70,16 +70,19 @@ public:
   // Cancel, but also ensure that all work done in Init() is undone. Call this
   // only when the channel has failed to open, and so calling Cancel() on it
   // won't be sufficient.
   void CancelAndAbort(nsresult aStatus);
 
   // Called or dispatched by cancel for main thread only execution.
   void ContinueCancel(nsresult aStatus);
 
+  // Called or dispatched by EvictFromCache for main thread only execution.
+  void ContinueEvict();
+
   // Methods that get forwarded to the Image, or deferred until it's
   // instantiated.
   nsresult LockImage();
   nsresult UnlockImage();
   nsresult StartDecoding();
   nsresult RequestDecode();
 
   inline void SetInnerWindowID(uint64_t aInnerWindowId) {
@@ -140,16 +143,17 @@ private:
   friend class imgStatusTracker;
   friend class imgCacheExpirationTracker;
   friend class imgRequestNotifyRunnable;
 
   inline void SetLoadId(void *aLoadId) {
     mLoadId = aLoadId;
   }
   void Cancel(nsresult aStatus);
+  void EvictFromCache();
   void RemoveFromCache();
 
   nsresult GetSecurityInfo(nsISupports **aSecurityInfo);
 
   inline const char *GetMimeType() const {
     return mContentType.get();
   }
   inline nsIProperties *Properties() {
--- a/js/xpconnect/src/xpc.msg
+++ b/js/xpconnect/src/xpc.msg
@@ -145,16 +145,17 @@ XPC_MSG_DEF(NS_ERROR_ALREADY_CONNECTED  
 XPC_MSG_DEF(NS_ERROR_NOT_CONNECTED                  , "The connection does not exist")
 XPC_MSG_DEF(NS_ERROR_CONNECTION_REFUSED             , "The connection was refused")
 XPC_MSG_DEF(NS_ERROR_PROXY_CONNECTION_REFUSED       , "The connection to the proxy server was refused")
 XPC_MSG_DEF(NS_ERROR_NET_TIMEOUT                    , "The connection has timed out")
 XPC_MSG_DEF(NS_ERROR_OFFLINE                        , "The requested action could not be completed in the offline state")
 XPC_MSG_DEF(NS_ERROR_PORT_ACCESS_NOT_ALLOWED        , "Establishing a connection to an unsafe or otherwise banned port was prohibited")
 XPC_MSG_DEF(NS_ERROR_NET_RESET                      , "The connection was established, but no data was ever received")
 XPC_MSG_DEF(NS_ERROR_NET_INTERRUPT                  , "The connection was established, but the data transfer was interrupted")
+XPC_MSG_DEF(NS_ERROR_NET_PARTIAL_TRANSFER           , "A transfer was only partially done when it completed")
 XPC_MSG_DEF(NS_ERROR_NOT_RESUMABLE                  , "This request is not resumable, but it was tried to resume it, or to request resume-specific data")
 XPC_MSG_DEF(NS_ERROR_ENTITY_CHANGED                 , "It was attempted to resume the request, but the entity has changed in the meantime")
 XPC_MSG_DEF(NS_ERROR_REDIRECT_LOOP                  , "The request failed as a result of a detected redirection loop")
 XPC_MSG_DEF(NS_ERROR_UNSAFE_CONTENT_TYPE            , "The request failed because the content type returned by the server was not a type expected by the channel")
 XPC_MSG_DEF(NS_ERROR_REMOTE_XUL                     , "Attempt to access remote XUL document that is not in website's whitelist")
 
 XPC_MSG_DEF(NS_ERROR_FTP_LOGIN                      , "FTP error while logging in")
 XPC_MSG_DEF(NS_ERROR_FTP_CWD                        , "FTP error while changing directory")
--- a/netwerk/protocol/http/Http2Session.cpp
+++ b/netwerk/protocol/http/Http2Session.cpp
@@ -135,30 +135,35 @@ template void CopyAsNetwork32(char *dest
 template void CopyAsNetwork32(uint8_t *dest, uint32_t number);
 
 PLDHashOperator
 Http2Session::ShutdownEnumerator(nsAHttpTransaction *key,
                                  nsAutoPtr<Http2Stream> &stream,
                                  void *closure)
 {
   Http2Session *self = static_cast<Http2Session *>(closure);
+  nsresult result;
 
   // On a clean server hangup the server sets the GoAwayID to be the ID of
   // the last transaction it processed. If the ID of stream in the
   // local stream is greater than that it can safely be restarted because the
   // server guarantees it was not partially processed. Streams that have not
   // registered an ID haven't actually been sent yet so they can always be
   // restarted.
   if (self->mCleanShutdown &&
       (stream->StreamID() > self->mGoAwayID || !stream->HasRegisteredID())) {
-    self->CloseStream(stream, NS_ERROR_NET_RESET); // can be restarted
+    result = NS_ERROR_NET_RESET;  // can be restarted
+  } else if (stream->RecvdData()) {
+    result = NS_ERROR_NET_PARTIAL_TRANSFER;
   } else {
-    self->CloseStream(stream, NS_ERROR_ABORT);
+    result = NS_ERROR_ABORT;
   }
 
+  self->CloseStream(stream, result);
+
   return PL_DHASH_NEXT;
 }
 
 PLDHashOperator
 Http2Session::GoAwayEnumerator(nsAHttpTransaction *key,
                                nsAutoPtr<Http2Stream> &stream,
                                void *closure)
 {
@@ -2008,16 +2013,18 @@ Http2Session::ReadyToProcessDataFrame(en
   }
 
   LOG3(("Start Processing Data Frame. "
         "Session=%p Stream ID 0x%X Stream Ptr %p Fin=%d Len=%d",
         this, mInputFrameID, mInputFrameDataStream, mInputFrameFinal,
         mInputFrameDataSize));
   UpdateLocalRwin(mInputFrameDataStream, mInputFrameDataSize);
 
+  mInputFrameDataStream->SetRecvdData(true);
+
   return NS_OK;
 }
 
 // WriteSegments() is used to read data off the socket. Generally this is
 // just the http2 frame header and from there the appropriate *Stream
 // is identified from the Stream-ID. The http transaction associated with
 // that read then pulls in the data directly, which it will feed to
 // OnWriteSegment(). That function will gateway it into http and feed
@@ -2280,17 +2287,19 @@ Http2Session::WriteSegments(nsAHttpSegme
     nsresult streamCleanupCode;
 
     // There is no bounds checking on the error code.. we provide special
     // handling for a couple of cases and all others (including unknown) are
     // equivalent to cancel.
     if (mDownstreamRstReason == REFUSED_STREAM_ERROR) {
       streamCleanupCode = NS_ERROR_NET_RESET;      // can retry this 100% safely
     } else {
-      streamCleanupCode = NS_ERROR_NET_INTERRUPT;
+      streamCleanupCode = mInputFrameDataStream->RecvdData() ?
+        NS_ERROR_NET_PARTIAL_TRANSFER :
+        NS_ERROR_NET_INTERRUPT;
     }
 
     if (mDownstreamRstReason == COMPRESSION_ERROR)
       mShouldGoAway = true;
 
     // mInputFrameDataStream is reset by ChangeDownstreamState
     Http2Stream *stream = mInputFrameDataStream;
     ResetDownstreamState();
--- a/netwerk/protocol/http/Http2Stream.h
+++ b/netwerk/protocol/http/Http2Stream.h
@@ -64,16 +64,19 @@ public:
     return mTransaction ? mTransaction->LoadGroupConnectionInfo() : nullptr;
   }
 
   void Close(nsresult reason);
 
   void SetRecvdFin(bool aStatus);
   bool RecvdFin() { return mRecvdFin; }
 
+  void SetRecvdData(bool aStatus) { mReceivedData = aStatus ? 1 : 0; }
+  bool RecvdData() { return mReceivedData; }
+
   void SetSentFin(bool aStatus);
   bool SentFin() { return mSentFin; }
 
   void SetRecvdReset(bool aStatus);
   bool RecvdReset() { return mRecvdReset; }
 
   void SetSentReset(bool aStatus);
   bool SentReset() { return mSentReset; }
@@ -193,16 +196,19 @@ private:
   // Flag is set when the HTTP processor has more data to send
   // but has blocked in doing so.
   uint32_t                     mRequestBlockedOnRead : 1;
 
   // Flag is set after the response frame bearing the fin bit has
   // been processed. (i.e. after the server has closed).
   uint32_t                     mRecvdFin             : 1;
 
+  // Flag is set after 1st DATA frame has been passed to stream
+  uint32_t                     mReceivedData         : 1;
+
   // Flag is set after RST_STREAM has been received for this stream
   uint32_t                     mRecvdReset           : 1;
 
   // Flag is set after RST_STREAM has been generated for this stream
   uint32_t                     mSentReset            : 1;
 
   // Flag is set when stream is counted towards MAX_CONCURRENT streams in session
   uint32_t                     mCountAsActive        : 1;
--- a/netwerk/protocol/http/SpdySession3.cpp
+++ b/netwerk/protocol/http/SpdySession3.cpp
@@ -1985,21 +1985,26 @@ SpdySession3::WriteSegments(nsAHttpSegme
             mInputFrameDataSize));
       UpdateLocalRwin(mInputFrameDataStream, mInputFrameDataSize);
     }
   }
 
   if (mDownstreamState == PROCESSING_CONTROL_RST_STREAM) {
     if (mDownstreamRstReason == RST_REFUSED_STREAM)
       rv = NS_ERROR_NET_RESET;            //we can retry this 100% safely
-    else if (mDownstreamRstReason == RST_CANCEL ||
-             mDownstreamRstReason == RST_PROTOCOL_ERROR ||
+    else if (mDownstreamRstReason == RST_CANCEL) {
+      rv = mInputFrameDataStream->RecvdData() ?
+        NS_ERROR_NET_PARTIAL_TRANSFER :
+        NS_ERROR_NET_INTERRUPT;
+    }
+    else if (mDownstreamRstReason == RST_PROTOCOL_ERROR ||
              mDownstreamRstReason == RST_INTERNAL_ERROR ||
-             mDownstreamRstReason == RST_UNSUPPORTED_VERSION)
+             mDownstreamRstReason == RST_UNSUPPORTED_VERSION) {
       rv = NS_ERROR_NET_INTERRUPT;
+    }
     else if (mDownstreamRstReason == RST_FRAME_TOO_LARGE)
       rv = NS_ERROR_FILE_TOO_BIG;
     else
       rv = NS_ERROR_ILLEGAL_VALUE;
 
     if (mDownstreamRstReason != RST_REFUSED_STREAM &&
         mDownstreamRstReason != RST_CANCEL)
       mShouldGoAway = true;
--- a/netwerk/protocol/http/SpdySession31.cpp
+++ b/netwerk/protocol/http/SpdySession31.cpp
@@ -2053,21 +2053,26 @@ SpdySession31::WriteSegments(nsAHttpSegm
             mInputFrameDataSize));
       UpdateLocalRwin(mInputFrameDataStream, mInputFrameDataSize);
     }
   }
 
   if (mDownstreamState == PROCESSING_CONTROL_RST_STREAM) {
     if (mDownstreamRstReason == RST_REFUSED_STREAM)
       rv = NS_ERROR_NET_RESET;            //we can retry this 100% safely
-    else if (mDownstreamRstReason == RST_CANCEL ||
-             mDownstreamRstReason == RST_PROTOCOL_ERROR ||
+    else if (mDownstreamRstReason == RST_CANCEL) {
+      rv = mInputFrameDataStream->RecvdData() ?
+        NS_ERROR_NET_PARTIAL_TRANSFER :
+        NS_ERROR_NET_INTERRUPT;
+    }
+    else if (mDownstreamRstReason == RST_PROTOCOL_ERROR ||
              mDownstreamRstReason == RST_INTERNAL_ERROR ||
-             mDownstreamRstReason == RST_UNSUPPORTED_VERSION)
+             mDownstreamRstReason == RST_UNSUPPORTED_VERSION) {
       rv = NS_ERROR_NET_INTERRUPT;
+    }
     else if (mDownstreamRstReason == RST_FRAME_TOO_LARGE)
       rv = NS_ERROR_FILE_TOO_BIG;
     else
       rv = NS_ERROR_ILLEGAL_VALUE;
 
     if (mDownstreamRstReason != RST_REFUSED_STREAM &&
         mDownstreamRstReason != RST_CANCEL)
       mShouldGoAway = true;
--- a/netwerk/protocol/http/nsHttpTransaction.cpp
+++ b/netwerk/protocol/http/nsHttpTransaction.cpp
@@ -862,16 +862,26 @@ nsHttpTransaction::Close(nsresult reason
 
             gHttpHandler->ConnMgr()->PipelineFeedbackInfo(
                 mConnInfo, nsHttpConnectionMgr::RedCorruptedContent, nullptr, 0);
             if (NS_SUCCEEDED(RestartInProgress()))
                 return;
         }
     }
 
+    if ((mChunkedDecoder || (mContentLength >= int64_t(0))) &&
+        (mHttpVersion >= NS_HTTP_VERSION_1_1)) {
+
+        if (NS_SUCCEEDED(reason) && !mResponseIsComplete) {
+            reason = NS_ERROR_NET_PARTIAL_TRANSFER;
+            LOG(("Partial transfer, incomplete HTTP responese received: %s",
+                 mChunkedDecoder ? "broken chunk" : "c-l underrun"));
+        }
+    }
+
     bool relConn = true;
     if (NS_SUCCEEDED(reason)) {
         if (!mResponseIsComplete) {
             // The response has not been delimited with a high-confidence
             // algorithm like Content-Length or Chunked Encoding. We
             // need to use a strong framing mechanism to pipeline.
             gHttpHandler->ConnMgr()->PipelineFeedbackInfo(
                 mConnInfo, nsHttpConnectionMgr::BadInsufficientFraming,
--- a/netwerk/test/unit/head_channels.js
+++ b/netwerk/test/unit/head_channels.js
@@ -24,16 +24,17 @@ function read_stream(stream, count) {
 const CL_EXPECT_FAILURE = 0x1;
 const CL_EXPECT_GZIP = 0x2;
 const CL_EXPECT_3S_DELAY = 0x4;
 const CL_SUSPEND = 0x8;
 const CL_ALLOW_UNKNOWN_CL = 0x10;
 const CL_EXPECT_LATE_FAILURE = 0x20;
 const CL_FROM_CACHE = 0x40; // Response must be from the cache
 const CL_NOT_FROM_CACHE = 0x80; // Response must NOT be from the cache
+const CL_IGNORE_CL = 0x100; // don't bother to verify the content-length
 
 const SUSPEND_DELAY = 3000;
 
 /**
  * A stream listener that calls a callback function with a specified
  * context and the received data when the channel is loaded.
  *
  * Signature of the closure:
@@ -151,17 +152,17 @@ ChannelListener.prototype = {
       if ((this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE)) && success)
         do_throw("Should have failed to load URL (status is " + status.toString(16) + ")");
       else if (!(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE)) && !success)
         do_throw("Failed to load URL: " + status.toString(16));
       if (status != request.status)
         do_throw("request.status does not match status arg to onStopRequest!");
       if (request.isPending())
         do_throw("request reports itself as pending from onStopRequest!");
-      if (!(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE)) &&
+      if (!(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE | CL_IGNORE_CL)) &&
           !(this._flags & CL_EXPECT_GZIP) &&
           this._contentLen != -1)
           do_check_eq(this._buffer.length, this._contentLen)
     } catch (ex) {
       do_throw("Error in onStopRequest: " + ex);
     }
     try {
       this._closure(request, this._buffer, this._closurectx);
--- a/netwerk/test/unit/test_chunked_responses.js
+++ b/netwerk/test/unit/test_chunked_responses.js
@@ -100,17 +100,17 @@ function completeTest2(request, data, ct
 
 ////////////////////////////////////////////////////////////////////////////////
 // Test 3: OK in spite of non-hex digits after size in the length field
 
 test_flags[3] = CL_ALLOW_UNKNOWN_CL;
 
 function handler3(metadata, response)
 {
-  var body = "c junkafter\r\ndata reached";
+  var body = "c junkafter\r\ndata reached\r\n0\r\n\r\n";
 
   response.seizePower();
   response.write("HTTP/1.1 200 OK\r\n");
   response.write("Content-Type: text/plain\r\n");
   response.write("Transfer-Encoding: chunked\r\n");
   response.write("\r\n");
   response.write(body);
   response.finish();
@@ -124,17 +124,17 @@ function completeTest3(request, data, ct
 
 ////////////////////////////////////////////////////////////////////////////////
 // Test 4: Verify a fully compliant chunked response.
 
 test_flags[4] = CL_ALLOW_UNKNOWN_CL;
 
 function handler4(metadata, response)
 {
-  var body = "c\r\ndata reached\r\n\0\r\n\r\n";
+  var body = "c\r\ndata reached\r\n3\r\nhej\r\n0\r\n\r\n";
 
   response.seizePower();
   response.write("HTTP/1.1 200 OK\r\n");
   response.write("Content-Type: text/plain\r\n");
   response.write("Transfer-Encoding: chunked\r\n");
   response.write("\r\n");
   response.write(body);
   response.finish();
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_content_length_underrun.js
@@ -0,0 +1,98 @@
+/*
+ * Test Content-Length underrun behavior
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+// Test infrastructure
+
+Cu.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+  return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+var httpserver = new HttpServer();
+var index = 0;
+var test_flags = new Array();
+var testPathBase = "/cl_hdrs";
+
+function run_test()
+{
+  httpserver.start(-1);
+
+  do_test_pending();
+  run_test_number(1);
+}
+
+function run_test_number(num)
+{
+  testPath = testPathBase + num;
+  httpserver.registerPathHandler(testPath, eval("handler" + num));
+
+  var channel = setupChannel(testPath);
+  flags = test_flags[num];   // OK if flags undefined for test
+  channel.asyncOpen(new ChannelListener(eval("completeTest" + num),
+                                        channel, flags), null);
+}
+
+function setupChannel(url)
+{
+  var ios = Components.classes["@mozilla.org/network/io-service;1"].
+                       getService(Ci.nsIIOService);
+  var chan = ios.newChannel(URL + url, "", null);
+  var httpChan = chan.QueryInterface(Components.interfaces.nsIHttpChannel);
+  return httpChan;
+}
+
+function endTests()
+{
+  httpserver.stop(do_test_finished);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 1: FAIL because of Content-Length underrun with HTTP 1.1
+test_flags[1] = CL_EXPECT_LATE_FAILURE;
+
+function handler1(metadata, response)
+{
+  var body = "blablabla";
+
+  response.seizePower();
+  response.write("HTTP/1.1 200 OK\r\n");
+  response.write("Content-Type: text/plain\r\n");
+  response.write("Content-Length: 556677\r\n");
+  response.write("\r\n");
+  response.write(body);
+  response.finish();
+}
+
+function completeTest1(request, data, ctx)
+{
+  do_check_eq(request.status, Components.results.NS_ERROR_NET_PARTIAL_TRANSFER);
+
+  run_test_number(2);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Test 2: Succeed because Content-Length underrun is with HTTP 1.0
+
+test_flags[2] = CL_IGNORE_CL;
+
+function handler2(metadata, response)
+{
+  var body = "short content";
+
+  response.seizePower();
+  response.write("HTTP/1.0 200 OK\r\n");
+  response.write("Content-Type: text/plain\r\n");
+  response.write("Content-Length: 12345678\r\n");
+  response.write("\r\n");
+  response.write(body);
+  response.finish();
+}
+
+function completeTest2(request, data, ctx)
+{
+  do_check_eq(request.status, Components.results.NS_OK);
+  endTests();
+}
--- a/netwerk/test/unit/test_gzipped_206.js
+++ b/netwerk/test/unit/test_gzipped_206.js
@@ -16,51 +16,53 @@ function make_channel(url, callback, ctx
 
 var doRangeResponse = false;
 
 function cachedHandler(metadata, response) {
   response.setHeader("Content-Type", "application/x-gzip", false);
   response.setHeader("Content-Encoding", "gzip", false);
   response.setHeader("ETag", "Just testing");
   response.setHeader("Cache-Control", "max-age=3600000"); // avoid validation
-  response.setHeader("Content-Length", "" + responseBody.length);
 
   var body = responseBody;
 
   if (doRangeResponse) {
     do_check_true(metadata.hasHeader("Range"));
     var matches = metadata.getHeader("Range").match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
     var from = (matches[1] === undefined) ? 0 : matches[1];
     var to = (matches[2] === undefined) ? responseBody.length - 1 : matches[2];
     if (from >= responseBody.length) {
       response.setStatusLine(metadata.httpVersion, 416, "Start pos too high");
       response.setHeader("Content-Range", "*/" + responseBody.length, false);
       return;
     }
     body = body.slice(from, to + 1);
+    response.setHeader("Content-Length", "" + (to + 1 - from));
     // always respond to successful range requests with 206
     response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
     response.setHeader("Content-Range", from + "-" + to + "/" + responseBody.length, false);
   } else {
+    // This response will get cut off prematurely
+    response.setHeader("Content-Length", "" + responseBody.length);
     response.setHeader("Accept-Ranges", "bytes");
     body = body.slice(0, 17); // slice off a piece to send first
     doRangeResponse = true;
   }
 
   var bos = Cc["@mozilla.org/binaryoutputstream;1"]
       .createInstance(Ci.nsIBinaryOutputStream);
   bos.setOutputStream(response.bodyOutputStream);
 
   response.processAsync();
   bos.writeByteArray(body, body.length);
   response.finish();
 }
 
 function continue_test(request, data) {
-  do_check_true(17 == data.length);
+  do_check_eq(17, data.length);
   var chan = make_channel("http://localhost:" +
                           httpserver.identity.primaryPort + "/cached/test.gz");
   chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_GZIP), null);
 }
 
 function finish_test(request, data, ctx) {
   do_check_eq(request.status, 0);
   do_check_eq(data.length, responseBody.length);
@@ -75,11 +77,11 @@ function run_test() {
   httpserver.registerPathHandler("/cached/test.gz", cachedHandler);
   httpserver.start(-1);
 
   // wipe out cached content
   evict_cache_entries();
 
   var chan = make_channel("http://localhost:" +
                           httpserver.identity.primaryPort + "/cached/test.gz");
-  chan.asyncOpen(new ChannelListener(continue_test, null, CL_EXPECT_GZIP), null);
+  chan.asyncOpen(new ChannelListener(continue_test, null, CL_EXPECT_GZIP|CL_EXPECT_LATE_FAILURE), null);
   do_test_pending();
 }
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -193,16 +193,17 @@ skip-if = os == "android"
 [test_cookiejars.js]
 [test_cookiejars_safebrowsing.js]
 [test_data_protocol.js]
 [test_dns_service.js]
 [test_dns_localredirect.js]
 [test_dns_proxy_bypass.js]
 [test_duplicate_headers.js]
 [test_chunked_responses.js]
+[test_content_length_underrun.js]
 [test_event_sink.js]
 [test_extract_charset_from_content_type.js]
 [test_force_sniffing.js]
 [test_fallback_no-cache-entry_canceled.js]
 [test_fallback_no-cache-entry_passing.js]
 [test_fallback_redirect-to-different-origin_canceled.js]
 [test_fallback_redirect-to-different-origin_passing.js]
 [test_fallback_request-error_canceled.js]
--- a/xpcom/base/ErrorList.h
+++ b/xpcom/base/ErrorList.h
@@ -196,16 +196,18 @@
    * banned port. */
   ERROR(NS_ERROR_PORT_ACCESS_NOT_ALLOWED,   FAILURE(19)),
   /* The connection was established, but no data was ever received. */
   ERROR(NS_ERROR_NET_RESET,                 FAILURE(20)),
   /* The connection was established, but the data transfer was interrupted. */
   ERROR(NS_ERROR_NET_INTERRUPT,             FAILURE(71)),
   /* The connection attempt to a proxy failed. */
   ERROR(NS_ERROR_PROXY_CONNECTION_REFUSED,  FAILURE(72)),
+  /* A transfer was only partially done when it completed. */
+  ERROR(NS_ERROR_NET_PARTIAL_TRANSFER,      FAILURE(76)),
 
   /* XXX really need to better rationalize these error codes.  are consumers of
    * necko really expected to know how to discern the meaning of these?? */
   /* This request is not resumable, but it was tried to resume it, or to
    * request resume-specific data. */
   ERROR(NS_ERROR_NOT_RESUMABLE,        FAILURE(25)),
   /* The request failed as a result of a detected redirection loop.  */
   ERROR(NS_ERROR_REDIRECT_LOOP,        FAILURE(31)),