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
treeherdermozilla-inbound@3fc647b93114 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcmanus, seth
bugs237623
milestone33.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 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)),