Bug 1321612, r=valentin
authorHonza Bambas <honzab.moz@firemni.cz>
Wed, 08 Feb 2017 15:48:59 +0100
changeset 341353 f99ea4baeaff2ee4c049eb2aaf273f6c50620cfe
parent 341352 9066a2ed849311d798300629edac248e81978af5
child 341354 250c680482eb1be0847932509c5ed3f5e121fd26
push id86694
push usercbook@mozilla.com
push dateWed, 08 Feb 2017 14:56:12 +0000
treeherdermozilla-inbound@250c680482eb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvalentin
bugs1321612
milestone54.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 1321612, r=valentin
netwerk/streamconv/converters/nsMultiMixedConv.cpp
netwerk/streamconv/converters/nsMultiMixedConv.h
netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js
netwerk/test/unit/test_multipart_streamconv.js
netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js
netwerk/test/unit/xpcshell.ini
--- a/netwerk/streamconv/converters/nsMultiMixedConv.cpp
+++ b/netwerk/streamconv/converters/nsMultiMixedConv.cpp
@@ -15,34 +15,17 @@
 #include "nsIStreamConverterService.h"
 #include "nsICacheInfoChannel.h"
 #include <algorithm>
 #include "nsContentSecurityManager.h"
 #include "nsHttp.h"
 #include "nsNetUtil.h"
 #include "nsIURI.h"
 #include "nsHttpHeaderArray.h"
-
-//
-// Helper function for determining the length of data bytes up to
-// the next multipart token.  A token is usually preceded by a LF
-// or CRLF delimiter.
-// 
-static uint32_t
-LengthToToken(const char *cursor, const char *token)
-{
-    uint32_t len = token - cursor;
-    // Trim off any LF or CRLF preceding the token
-    if (len && *(token-1) == '\n') {
-        --len;
-        if (len && *(token-2) == '\r')
-            --len;
-    }
-    return len;
-}
+#include "mozilla/AutoRestore.h"
 
 nsPartChannel::nsPartChannel(nsIChannel *aMultipartChannel, uint32_t aPartID,
                              nsIStreamListener* aListener) :
   mMultipartChannel(aMultipartChannel),
   mListener(aListener),
   mStatus(NS_OK),
   mContentLength(UINT64_MAX),
   mIsByteRangeRequest(false),
@@ -471,371 +454,383 @@ nsMultiMixedConv::AsyncConvertData(const
     // WARNING: this listener must be able to handle multiple OnStartRequest, OnDataAvail()
     //  and OnStopRequest() call combinations. We call of series of these for each sub-part
     //  in the raw stream.
     mFinalListener = aListener;
 
     return NS_OK;
 }
 
-// AutoFree implementation to prevent memory leaks
-class AutoFree
+// nsIRequestObserver implementation
+NS_IMETHODIMP
+nsMultiMixedConv::OnStartRequest(nsIRequest *request, nsISupports *ctxt)
 {
-public:
-  AutoFree() : mBuffer(nullptr) {}
+    // we're assuming the content-type is available at this stage
+    NS_ASSERTION(mBoundary.IsEmpty(), "a second on start???");
+
+    nsresult rv;
+
+    mContext = ctxt;
+    mTotalSent   = 0;
+    mChannel = do_QueryInterface(request, &rv);
+    if (NS_FAILED(rv)) return rv;
 
-  explicit AutoFree(char *buffer) : mBuffer(buffer) {}
+    nsAutoCString contentType;
 
-  ~AutoFree() {
-    free(mBuffer);
-  }
+    // ask the HTTP channel for the content-type and extract the boundary from it.
+    nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv);
+    if (NS_SUCCEEDED(rv)) {
+        rv = httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("content-type"), contentType);
+        if (NS_FAILED(rv)) {
+            return rv;
+        }
+    } else {
+        // try asking the channel directly
+        rv = mChannel->GetContentType(contentType);
+        if (NS_FAILED(rv)) {
+            return NS_ERROR_FAILURE;
+        }
+    }
 
-  AutoFree& operator=(char *buffer) {
-    mBuffer = buffer;
-    return *this;
-  }
+    Tokenizer p(contentType);
+    p.SkipUntil(Token::Char(';'));
+    if (!p.CheckChar(';')) {
+        return NS_ERROR_CORRUPTED_CONTENT;
+    }
+    p.SkipWhites();
+    if (!p.CheckWord("boundary")) {
+        return NS_ERROR_CORRUPTED_CONTENT;
+    }
+    p.SkipWhites();
+    if (!p.CheckChar('=')) {
+        return NS_ERROR_CORRUPTED_CONTENT;
+    }
+    p.SkipWhites();
+    Unused << p.ReadUntil(Token::Char(';'), mBoundary);
+    mBoundary.Trim(" \""); // ignoring potential quoted string formatting violations
+    if (mBoundary.IsEmpty()) {
+        return NS_ERROR_CORRUPTED_CONTENT;
+    }
 
-  operator char*() const {
-    return mBuffer;
-  }
-private:
-  char *mBuffer;
-};
+    mHeaderTokens[HEADER_CONTENT_TYPE] =
+      mTokenizer.AddCustomToken("content-type", mTokenizer.CASE_INSENSITIVE, false);
+    mHeaderTokens[HEADER_CONTENT_LENGTH] =
+      mTokenizer.AddCustomToken("content-length", mTokenizer.CASE_INSENSITIVE, false);
+    mHeaderTokens[HEADER_CONTENT_DISPOSITION] =
+      mTokenizer.AddCustomToken("content-disposition", mTokenizer.CASE_INSENSITIVE, false);
+    mHeaderTokens[HEADER_SET_COOKIE] =
+      mTokenizer.AddCustomToken("set-cookie", mTokenizer.CASE_INSENSITIVE, false);
+    mHeaderTokens[HEADER_CONTENT_RANGE] =
+      mTokenizer.AddCustomToken("content-range", mTokenizer.CASE_INSENSITIVE, false);
+    mHeaderTokens[HEADER_RANGE] =
+      mTokenizer.AddCustomToken("range", mTokenizer.CASE_INSENSITIVE, false);
+
+    mLFToken = mTokenizer.AddCustomToken("\n", mTokenizer.CASE_SENSITIVE, false);
+    mCRLFToken = mTokenizer.AddCustomToken("\r\n", mTokenizer.CASE_SENSITIVE, false);
+
+    SwitchToControlParsing();
+
+    mBoundaryToken =
+      mTokenizer.AddCustomToken(mBoundary, mTokenizer.CASE_SENSITIVE);
+    mBoundaryTokenWithDashes =
+      mTokenizer.AddCustomToken(NS_LITERAL_CSTRING("--") + mBoundary, mTokenizer.CASE_SENSITIVE);
+
+    return NS_OK;
+}
 
 // nsIStreamListener implementation
 NS_IMETHODIMP
 nsMultiMixedConv::OnDataAvailable(nsIRequest *request, nsISupports *context,
                                   nsIInputStream *inStr, uint64_t sourceOffset,
-                                  uint32_t count) {
-    nsresult rv = NS_OK;
-    AutoFree buffer(nullptr);
-    uint32_t bufLen = 0, read = 0;
-
-    NS_ASSERTION(request, "multimixed converter needs a request");
-
-    nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
-    if (NS_FAILED(rv)) return rv;
-
-    // fill buffer
-    {
-        bufLen = count + mBufLen;
-        NS_ENSURE_TRUE((bufLen >= count) && (bufLen >= mBufLen),
-                       NS_ERROR_FAILURE);
-        buffer = (char *) malloc(bufLen);
-        if (!buffer)
-            return NS_ERROR_OUT_OF_MEMORY;
-
-        if (mBufLen) {
-            // incorporate any buffered data into the parsing
-            memcpy(buffer, mBuffer, mBufLen);
-            free(mBuffer);
-            mBuffer = 0;
-            mBufLen = 0;
-        }
-        
-        rv = inStr->Read(buffer + (bufLen - count), count, &read);
-
-        if (NS_FAILED(rv) || read == 0) return rv;
-        NS_ASSERTION(read == count, "poor data size assumption");
-    }
-
-    char *cursor = buffer;
-
-    if (mFirstOnData) {
-        // this is the first OnData() for this request. some servers
-        // don't bother sending a token in the first "part." This is
-        // illegal, but we'll handle the case anyway by shoving the
-        // boundary token in for the server.
-        mFirstOnData = false;
-        NS_ASSERTION(!mBufLen, "this is our first time through, we can't have buffered data");
-        const char * token = mToken.get();
+                                  uint32_t count)
+{
+    // Failing these assertions may indicate that some of the target listeners of this converter
+    // is looping the thead queue, which is harmful to how we collect the raw (content) data.
+    MOZ_DIAGNOSTIC_ASSERT(!mInOnDataAvailable, "nsMultiMixedConv::OnDataAvailable reentered!");
+    MOZ_DIAGNOSTIC_ASSERT(!mRawData, "There are unsent data from the previous tokenizer feed!");
 
-        PushOverLine(cursor, bufLen);
-
-        bool needMoreChars = bufLen < mTokenLen + 2;
-        nsAutoCString firstBuffer(buffer, bufLen);
-        int32_t posCR = firstBuffer.Find("\r");
-
-        if (needMoreChars || (posCR == kNotFound)) {
-            // we don't have enough data yet to make this comparison.
-            // skip this check, and try again the next time OnData()
-            // is called.
-            mFirstOnData = true;
-        } else if (!PL_strnstr(cursor, token, mTokenLen + 2)) {
-            char *newBuffer = (char *) realloc(buffer, bufLen + mTokenLen + 1);
-            if (!newBuffer)
-                return NS_ERROR_OUT_OF_MEMORY;
-            buffer = newBuffer;
-
-            memmove(buffer + mTokenLen + 1, buffer, bufLen);
-            memcpy(buffer, token, mTokenLen);
-            buffer[mTokenLen] = '\n';
-
-            bufLen += (mTokenLen + 1);
-
-            // need to reset cursor to the buffer again (bug 100595)
-            cursor = buffer;
-        }
-    }
-
-    char *token = nullptr;
-
-    if (mProcessingHeaders) {
-        // we were not able to process all the headers
-        // for this "part" given the previous buffer given to 
-        // us in the previous OnDataAvailable callback.
-        bool done = false;
-        rv = ParseHeaders(channel, cursor, bufLen, &done);
-        if (NS_FAILED(rv)) return rv;
-
-        if (done) {
-            mProcessingHeaders = false;
-            rv = SendStart(channel);
-            if (NS_FAILED(rv)) return rv;
-        }
+    if (mInOnDataAvailable) {
+        // The multipart logic is incapable of being reentered.
+        return NS_ERROR_UNEXPECTED;
     }
 
-    int32_t tokenLinefeed = 1;
-    while ( (token = FindToken(cursor, bufLen)) ) {
-
-        if (((token + mTokenLen) < (cursor + bufLen)) &&
-            (*(token + mTokenLen + 1) == '-')) {
-            // This was the last delimiter so we can stop processing
-            rv = SendData(cursor, LengthToToken(cursor, token));
-            if (NS_FAILED(rv)) return rv;
-            if (mPartChannel) {
-                mPartChannel->SetIsLastPart();
-            }
-            return SendStop(NS_OK);
-        }
-
-        if (!mNewPart && token > cursor) {
-            // headers are processed, we're pushing data now.
-            NS_ASSERTION(!mProcessingHeaders, "we should be pushing raw data");
-            rv = SendData(cursor, LengthToToken(cursor, token));
-            bufLen -= token - cursor;
-            if (NS_FAILED(rv)) return rv;
-        }
-        // XXX else NS_ASSERTION(token == cursor, "?");
-        token += mTokenLen;
-        bufLen -= mTokenLen;
-        tokenLinefeed = PushOverLine(token, bufLen);
-
-        if (mNewPart) {
-            // parse headers
-            mNewPart = false;
-            cursor = token;
-            bool done = false;
-            rv = ParseHeaders(channel, cursor, bufLen, &done);
-            if (NS_FAILED(rv)) return rv;
-
-            if (done) {
-                rv = SendStart(channel);
-                if (NS_FAILED(rv)) return rv;
-            }
-            else {
-                // we haven't finished processing header info.
-                // we'll break out and try to process later.
-                mProcessingHeaders = true;
-                break;
-            }
-        }
-        else {
-            mNewPart = true;
-            // Reset state so we don't carry it over from part to part
-            mContentType.Truncate();
-            mContentLength = UINT64_MAX;
-            mContentDisposition.Truncate();
-            mIsByteRangeRequest = false;
-            mByteRangeStart = 0;
-            mByteRangeEnd = 0;
-            
-            rv = SendStop(NS_OK);
-            if (NS_FAILED(rv)) return rv;
-            // reset the token to front. this allows us to treat
-            // the token as a starting token.
-            token -= mTokenLen + tokenLinefeed;
-            bufLen += mTokenLen + tokenLinefeed;
-            cursor = token;
-        }
-    }
-
-    // at this point, we want to buffer up whatever amount (bufLen)
-    // we have leftover. However, we *always* want to ensure that
-    // we buffer enough data to handle a broken token.
+    mozilla::AutoRestore<bool> restore(mInOnDataAvailable);
+    mInOnDataAvailable = true;
 
-    // carry over
-    uint32_t bufAmt = 0;
-    if (mProcessingHeaders)
-        bufAmt = bufLen;
-    else if (bufLen) {
-        // if the data ends in a linefeed, and we're in the middle
-        // of a "part" (ie. mPartChannel exists) don't bother
-        // buffering, go ahead and send the data we have. Otherwise
-        // if we don't have a channel already, then we don't even
-        // have enough info to start a part, go ahead and buffer
-        // enough to collect a boundary token.
-        if (!mPartChannel || !(cursor[bufLen-1] == nsCRT::LF) )
-            bufAmt = std::min(mTokenLen - 1, bufLen);
-    }
-
-    if (bufAmt) {
-        rv = BufferData(cursor + (bufLen - bufAmt), bufAmt);
-        if (NS_FAILED(rv)) return rv;
-        bufLen -= bufAmt;
-    }
-
-    if (bufLen) {
-        rv = SendData(cursor, bufLen);
-        if (NS_FAILED(rv)) return rv;
-    }
-
-    return rv;
-}
-
-
-// nsIRequestObserver implementation
-NS_IMETHODIMP
-nsMultiMixedConv::OnStartRequest(nsIRequest *request, nsISupports *ctxt) {
-    // we're assuming the content-type is available at this stage
-    NS_ASSERTION(mToken.IsEmpty(), "a second on start???");
-    const char *bndry = nullptr;
-    nsAutoCString delimiter;
-    nsresult rv = NS_OK;
-    mContext = ctxt;
-
-    mFirstOnData = true;
-    mTotalSent   = 0;
+    nsresult rv_feed = mTokenizer.FeedInput(inStr, count);
+    // We must do this every time.  Regardless if something has failed during the
+    // parsing process.  Otherwise the raw data reference would not be thrown away.
+    nsresult rv_send = SendData();
 
-    nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
-    if (NS_FAILED(rv)) return rv;
-
-    // ask the HTTP channel for the content-type and extract the boundary from it.
-    nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel, &rv);
-    if (NS_SUCCEEDED(rv)) {
-        rv = httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("content-type"), delimiter);
-        if (NS_FAILED(rv)) {
-            return rv;
-        }
-    } else {
-        // try asking the channel directly
-        rv = channel->GetContentType(delimiter);
-        if (NS_FAILED(rv)) {
-            return NS_ERROR_FAILURE;
-        }
-    }
-
-    bndry = strstr(delimiter.BeginWriting(), "boundary");
-
-    if (!bndry) {
-        return NS_ERROR_FAILURE;
-    }
-
-    bndry = strchr(bndry, '=');
-    if (!bndry) return NS_ERROR_FAILURE;
-
-    bndry++; // move past the equals sign
-
-    char *attrib = (char *) strchr(bndry, ';');
-    if (attrib) *attrib = '\0';
-
-    nsAutoCString boundaryString(bndry);
-    if (attrib) *attrib = ';';
-
-    boundaryString.Trim(" \"");
-
-    mToken = boundaryString;
-    mTokenLen = boundaryString.Length();
-
-    if (mTokenLen == 0) {
-        return NS_ERROR_FAILURE;
-    }
-
-    return NS_OK;
+    return NS_FAILED(rv_send) ? rv_send : rv_feed;
 }
 
 NS_IMETHODIMP
 nsMultiMixedConv::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
                                 nsresult aStatus)
 {
-    if (mToken.IsEmpty()) { // no token, no love.
+    nsresult rv;
+
+    if (mBoundary.IsEmpty()) { // no token, no love.
         return NS_ERROR_FAILURE;
     }
 
     if (mPartChannel) {
         mPartChannel->SetIsLastPart();
 
-        // we've already called SendStart() (which sets up the mPartChannel,
-        // and fires an OnStart()) send any data left over, and then fire the stop.
-        if (mBufLen > 0 && mBuffer) {
-            (void) SendData(mBuffer, mBufLen);
-            // don't bother checking the return value here, if the send failed
-            // we're done anyway as we're in the OnStop() callback.
-            free(mBuffer);
-            mBuffer = nullptr;
-            mBufLen = 0;
+        MOZ_DIAGNOSTIC_ASSERT(!mRawData, "There are unsent data from the previous tokenizer feed!");
+
+        rv = mTokenizer.FinishInput();
+        if (NS_SUCCEEDED(aStatus)) {
+            aStatus = rv;
         }
+        rv = SendData();
+        if (NS_SUCCEEDED(aStatus)) {
+            aStatus = rv;
+        }
+
         (void) SendStop(aStatus);
-    } else if (NS_FAILED(aStatus)) {
+    } else if (NS_FAILED(aStatus) && !mRequestListenerNotified) {
         // underlying data production problem. we should not be in
         // the middle of sending data. if we were, mPartChannel,
-        // above, would have been true.
+        // above, would have been non-null.
 
-        // if we send the start, the URI Loader's m_targetStreamListener, may
-        // be pointing at us causing a nice stack overflow.  So, don't call 
-        // OnStartRequest!  -  This breaks necko's semantecs. 
-        //(void) mFinalListener->OnStartRequest(request, ctxt);
-
+        (void) mFinalListener->OnStartRequest(request, ctxt);
         (void) mFinalListener->OnStopRequest(request, ctxt, aStatus);
     }
 
     return NS_OK;
 }
 
+nsresult
+nsMultiMixedConv::ConsumeToken(Token const & token)
+{
+  nsresult rv;
+
+  switch (mParserState) {
+    case BOUNDARY:
+      if (token.Equals(mBoundaryTokenWithDashes)) {
+        // The server first used boundary '--boundary'.  Hence, we no longer
+        // accept plain 'boundary' token as a delimiter.
+        mTokenizer.RemoveCustomToken(mBoundaryToken);
+        mParserState = BOUNDARY_CRLF;
+        break;
+      }
+      if (token.Equals(mBoundaryToken)) {
+        // And here the opposite from the just above block...
+        mTokenizer.RemoveCustomToken(mBoundaryTokenWithDashes);
+        mParserState = BOUNDARY_CRLF;
+        break;
+      }
+
+      // In case the server didn't send the first boundary...
+      // This may be either a line-feed or a header.  Turn header tokens
+      // on and retry in a shifted parser state.
+      mParserState = HEADER_NAME;
+      mResponseHeader = HEADER_UNKNOWN;
+      SetHeaderTokensEnabled(true);
+      mTokenizer.Rollback();
+      break;
+
+    case BOUNDARY_CRLF:
+      if (token.Equals(Token::NewLine())) {
+        mParserState = HEADER_NAME;
+        mResponseHeader = HEADER_UNKNOWN;
+        HeadersToDefault();
+        SetHeaderTokensEnabled(true);
+        break;
+      }
+      return NS_ERROR_CORRUPTED_CONTENT;
+
+    case HEADER_NAME:
+      SetHeaderTokensEnabled(false);
+      if (token.Equals(Token::NewLine())) {
+        mParserState = BODY_INIT;
+        SwitchToBodyParsing();
+        break;
+      }
+      for (uint32_t h = HEADER_CONTENT_TYPE; h < HEADER_UNKNOWN; ++h) {
+        if (token.Equals(mHeaderTokens[h])) {
+          mResponseHeader = static_cast<EHeader>(h);
+          break;
+        }
+      }
+      mParserState = HEADER_SEP;
+      break;
+
+    case HEADER_SEP:
+      if (token.Equals(Token::Char(':'))) {
+        mParserState = HEADER_VALUE;
+        mResponseHeaderValue.Truncate();
+        break;
+      }
+      if (mResponseHeader == HEADER_UNKNOWN) {
+        // If the header is not of any we understand, just pass everything till ':'
+        break;
+      }
+      if (token.Equals(Token::Whitespace())) {
+        // Accept only header-name traling whitespaces after known headers
+        break;
+      }
+      return NS_ERROR_CORRUPTED_CONTENT;
+
+    case HEADER_VALUE:
+      if (token.Equals(Token::Whitespace()) && mResponseHeaderValue.IsEmpty()) {
+        // Eat leading whitespaces
+        break;
+      }
+      if (token.Equals(Token::NewLine())) {
+        nsresult rv = ProcessHeader();
+        if (NS_FAILED(rv)) {
+          return rv;
+        }
+        mParserState = HEADER_NAME;
+        mResponseHeader = HEADER_UNKNOWN;
+        SetHeaderTokensEnabled(true);
+      } else {
+        mResponseHeaderValue.Append(token.Fragment());
+      }
+      break;
+
+    case BODY_INIT:
+      rv = SendStart();
+      if (NS_FAILED(rv)) {
+        return rv;
+      }
+      mParserState = BODY;
+      MOZ_FALLTHROUGH;
+
+    case BODY: {
+      if (!token.Equals(mLFToken) && !token.Equals(mCRLFToken)) {
+        if (token.Equals(mBoundaryTokenWithDashes) ||
+            token.Equals(mBoundaryToken)) {
+          // Allow CRLF to NOT be part of the boundary as well
+          SwitchToControlParsing();
+          mParserState = TRAIL_DASH1;
+          break;
+        }
+        AccumulateData(token);
+        break;
+      }
+
+      // After CRLF we must explicitly check for boundary.  If found,
+      // that CRLF is part of the boundary and must not be send to the
+      // data listener.
+      Token token2;
+      if (!mTokenizer.Next(token2)) {
+        // Note: this will give us the CRLF token again when more data
+        // or OnStopRequest arrive.  I.e. we will enter BODY case in
+        // the very same state as we are now and start this block over.
+        mTokenizer.NeedMoreInput();
+        break;
+      }
+      if (token2.Equals(mBoundaryTokenWithDashes) ||
+          token2.Equals(mBoundaryToken)) {
+        SwitchToControlParsing();
+        mParserState = TRAIL_DASH1;
+        break;
+      }
+
+      AccumulateData(token);
+      AccumulateData(token2);
+      break;
+    }
+
+    case TRAIL_DASH1:
+      if (token.Equals(Token::NewLine())) {
+        rv = SendStop(NS_OK);
+        if (NS_FAILED(rv)) {
+          return rv;
+        }
+        mParserState = BOUNDARY_CRLF;
+        mTokenizer.Rollback();
+        break;
+      }
+      if (token.Equals(Token::Char('-'))) {
+        mParserState = TRAIL_DASH2;
+        break;
+      }
+      return NS_ERROR_CORRUPTED_CONTENT;
+
+    case TRAIL_DASH2:
+      if (token.Equals(Token::Char('-'))) {
+        mPartChannel->SetIsLastPart();
+        // SendStop calls SendData first.
+        rv = SendStop(NS_OK);
+        if (NS_FAILED(rv)) {
+          return rv;
+        }
+        mParserState = EPILOGUE;
+        break;
+      }
+      return NS_ERROR_CORRUPTED_CONTENT;
+
+    case EPILOGUE:
+      // Just ignore
+      break;
+
+    default:
+      MOZ_ASSERT(false, "Missing parser state handling branch");
+      break;
+  } // switch
+
+  return NS_OK;
+}
+
+void
+nsMultiMixedConv::SetHeaderTokensEnabled(bool aEnable)
+{
+    for (uint32_t h = HEADER_FIRST; h < HEADER_UNKNOWN; ++h) {
+        mTokenizer.EnableCustomToken(mHeaderTokens[h], aEnable);
+    }
+}
+
+void
+nsMultiMixedConv::SwitchToBodyParsing()
+{
+    mTokenizer.SetTokenizingMode(Tokenizer::Mode::CUSTOM_ONLY);
+    mTokenizer.EnableCustomToken(mLFToken, true);
+    mTokenizer.EnableCustomToken(mCRLFToken, true);
+    mTokenizer.EnableCustomToken(mBoundaryTokenWithDashes, true);
+    mTokenizer.EnableCustomToken(mBoundaryToken, true);
+}
+
+void
+nsMultiMixedConv::SwitchToControlParsing()
+{
+    mTokenizer.SetTokenizingMode(Tokenizer::Mode::FULL);
+    mTokenizer.EnableCustomToken(mLFToken, false);
+    mTokenizer.EnableCustomToken(mCRLFToken, false);
+    mTokenizer.EnableCustomToken(mBoundaryTokenWithDashes, false);
+    mTokenizer.EnableCustomToken(mBoundaryToken, false);
+}
 
 // nsMultiMixedConv methods
 nsMultiMixedConv::nsMultiMixedConv() :
-  mCurrentPartID(0)
+    mCurrentPartID(0),
+    mInOnDataAvailable(false),
+    mTokenizer([this](Token const& token, mozilla::IncrementalTokenizer&) -> nsresult {
+        return this->ConsumeToken(token);
+    })
 {
-    mTokenLen           = 0;
-    mNewPart            = true;
     mContentLength      = UINT64_MAX;
-    mBuffer             = nullptr;
-    mBufLen             = 0;
-    mProcessingHeaders  = false;
     mByteRangeStart     = 0;
     mByteRangeEnd       = 0;
     mTotalSent          = 0;
     mIsByteRangeRequest = false;
+    mParserState        = INIT;
+    mRawData            = nullptr;
+    mRequestListenerNotified = false;
 }
 
-nsMultiMixedConv::~nsMultiMixedConv() {
-    NS_ASSERTION(!mBuffer, "all buffered data should be gone");
-    if (mBuffer) {
-        free(mBuffer);
-        mBuffer = nullptr;
-    }
-}
+nsMultiMixedConv::~nsMultiMixedConv() {}
 
 nsresult
-nsMultiMixedConv::BufferData(char *aData, uint32_t aLen) {
-    NS_ASSERTION(!mBuffer, "trying to over-write buffer");
-
-    char *buffer = (char *) malloc(aLen);
-    if (!buffer) return NS_ERROR_OUT_OF_MEMORY;
-
-    memcpy(buffer, aData, aLen);
-    mBuffer = buffer;
-    mBufLen = aLen;
-    return NS_OK;
-}
-
-
-nsresult
-nsMultiMixedConv::SendStart(nsIChannel *aChannel) {
+nsMultiMixedConv::SendStart()
+{
     nsresult rv = NS_OK;
 
     nsCOMPtr<nsIStreamListener> partListener(mFinalListener);
     if (mContentType.IsEmpty()) {
         mContentType.AssignLiteral(UNKNOWN_CONTENT_TYPE);
         nsCOMPtr<nsIStreamConverterService> serv =
             do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv);
         if (NS_SUCCEEDED(rv)) {
@@ -848,20 +843,20 @@ nsMultiMixedConv::SendStart(nsIChannel *
             if (NS_SUCCEEDED(rv)) {
                 partListener = converter;
             }
         }
     }
 
     // if we already have an mPartChannel, that means we never sent a Stop()
     // before starting up another "part." that would be bad.
-    NS_ASSERTION(!mPartChannel, "tisk tisk, shouldn't be overwriting a channel");
+    MOZ_ASSERT(!mPartChannel, "tisk tisk, shouldn't be overwriting a channel");
 
     nsPartChannel *newChannel;
-    newChannel = new nsPartChannel(aChannel, mCurrentPartID++, partListener);
+    newChannel = new nsPartChannel(mChannel, mCurrentPartID++, partListener);
     if (!newChannel)
         return NS_ERROR_OUT_OF_MEMORY;
 
     if (mIsByteRangeRequest) {
         newChannel->InitializeByteRange(mByteRangeStart, mByteRangeEnd);
     }
 
     mTotalSent = 0;
@@ -886,236 +881,185 @@ nsMultiMixedConv::SendStart(nsIChannel *
     (void)mPartChannel->GetLoadGroup(getter_AddRefs(loadGroup));
 
     // Add the new channel to the load group (if any)
     if (loadGroup) {
         rv = loadGroup->AddRequest(mPartChannel, nullptr);
         if (NS_FAILED(rv)) return rv;
     }
 
+    // This prevents artificial call to OnStart/StopRequest when the root
+    // channel fails.  Since now it's ensured to keep with the nsIStreamListener
+    // contract every time.
+    mRequestListenerNotified = true;
+
     // Let's start off the load. NOTE: we don't forward on the channel passed
     // into our OnDataAvailable() as it's the root channel for the raw stream.
     return mPartChannel->SendOnStartRequest(mContext);
 }
 
-
 nsresult
-nsMultiMixedConv::SendStop(nsresult aStatus) {
-    
-    nsresult rv = NS_OK;
+nsMultiMixedConv::SendStop(nsresult aStatus)
+{
+    // Make sure we send out all accumulcated data prior call to OnStopRequest.
+    // If there is no data, this is a no-op.
+    nsresult rv = SendData();
+    if (NS_SUCCEEDED(aStatus)) {
+        aStatus = rv;
+    }
     if (mPartChannel) {
         rv = mPartChannel->SendOnStopRequest(mContext, aStatus);
         // don't check for failure here, we need to remove the channel from 
         // the loadgroup.
 
         // Remove the channel from its load group (if any)
         nsCOMPtr<nsILoadGroup> loadGroup;
         (void) mPartChannel->GetLoadGroup(getter_AddRefs(loadGroup));
         if (loadGroup) 
             (void) loadGroup->RemoveRequest(mPartChannel, mContext, aStatus);
     }
 
     mPartChannel = nullptr;
     return rv;
 }
 
-nsresult
-nsMultiMixedConv::SendData(char *aBuffer, uint32_t aLen) {
+void
+nsMultiMixedConv::AccumulateData(Token const & aToken)
+{
+    if (!mRawData) {
+        // This is the first read of raw data during this FeedInput loop
+        // of the incremental tokenizer.  All 'raw' tokens are coming from
+        // the same linear buffer, hence begining of this loop raw data
+        // is begining of the first raw token.  Length of this loop raw
+        // data is just sum of all 'raw' tokens we collect during this loop.
+        //
+        // It's ensured we flush (send to to the listener via OnDataAvailable)
+        // and nullify the collected raw data right after FeedInput call.
+        // Hence, the reference can't outlive the actual buffer.
+        mRawData = aToken.Fragment().BeginReading();
+        mRawDataLength = 0;
+    }
 
-    nsresult rv = NS_OK;
-    
-    if (!mPartChannel) return NS_ERROR_FAILURE; // something went wrong w/ processing
+    mRawDataLength += aToken.Fragment().Length();
+}
+
+nsresult
+nsMultiMixedConv::SendData()
+{
+    nsresult rv;
+
+    if (!mRawData) {
+        return NS_OK;
+    }
+
+    nsACString::const_char_iterator rawData = mRawData;
+    mRawData = nullptr;
+
+    if (!mPartChannel) {
+        return NS_ERROR_FAILURE; // something went wrong w/ processing
+    }
 
     if (mContentLength != UINT64_MAX) {
         // make sure that we don't send more than the mContentLength
         // XXX why? perhaps the Content-Length header was actually wrong!!
-        if ((uint64_t(aLen) + mTotalSent) > mContentLength)
-            aLen = static_cast<uint32_t>(mContentLength - mTotalSent);
+        if ((uint64_t(mRawDataLength) + mTotalSent) > mContentLength)
+            mRawDataLength = static_cast<uint32_t>(mContentLength - mTotalSent);
 
-        if (aLen == 0)
+        if (mRawDataLength == 0)
             return NS_OK;
     }
 
     uint64_t offset = mTotalSent;
-    mTotalSent += aLen;
+    mTotalSent += mRawDataLength;
 
     nsCOMPtr<nsIStringInputStream> ss(
             do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv));
     if (NS_FAILED(rv))
         return rv;
 
-    rv = ss->ShareData(aBuffer, aLen);
+    rv = ss->ShareData(rawData, mRawDataLength);
+    mRawData = nullptr;
     if (NS_FAILED(rv))
         return rv;
 
     nsCOMPtr<nsIInputStream> inStream(do_QueryInterface(ss, &rv));
     if (NS_FAILED(rv)) return rv;
 
-    return mPartChannel->SendOnDataAvailable(mContext, inStream, offset, aLen);
+    return mPartChannel->SendOnDataAvailable(mContext, inStream, offset, mRawDataLength);
 }
 
-int32_t
-nsMultiMixedConv::PushOverLine(char *&aPtr, uint32_t &aLen) {
-    int32_t chars = 0;
-    if ((aLen > 0) && (*aPtr == nsCRT::CR || *aPtr == nsCRT::LF)) {
-        if ((aLen > 1) && (aPtr[1] == nsCRT::LF))
-            chars++;
-        chars++;
-        aPtr += chars;
-        aLen -= chars;
-    }
-    return chars;
+void
+nsMultiMixedConv::HeadersToDefault()
+{
+    mContentLength = UINT64_MAX;
+    mContentType.Truncate();
+    mContentDisposition.Truncate();
+    mIsByteRangeRequest = false;
 }
 
 nsresult
-nsMultiMixedConv::ParseHeaders(nsIChannel *aChannel, char *&aPtr, 
-                               uint32_t &aLen, bool *_retval) {
-    // NOTE: this data must be ascii.
-    // NOTE: aPtr is NOT null terminated!
-    nsresult rv = NS_OK;
-    char *cursor = aPtr, *newLine = nullptr;
-    uint32_t cursorLen = aLen;
-    bool done = false;
-    uint32_t lineFeedIncrement = 1;
-
-    mContentLength = UINT64_MAX; // XXX what if we were already called?
-    while (cursorLen && (newLine = (char *) memchr(cursor, nsCRT::LF, cursorLen))) {
-        // adjust for linefeeds
-        if ((newLine > cursor) && (newLine[-1] == nsCRT::CR) ) { // CRLF
-            lineFeedIncrement = 2;
-            newLine--;
-        }
-        else
-            lineFeedIncrement = 1; // reset
-
-        if (newLine == cursor) {
-            // move the newLine beyond the linefeed marker
-            NS_ASSERTION(cursorLen >= lineFeedIncrement, "oops!");
-
-            cursor += lineFeedIncrement;
-            cursorLen -= lineFeedIncrement;
-
-            done = true;
-            break;
-        }
-
-        char tmpChar = *newLine;
-        *newLine = '\0'; // cursor is now null terminated
-
-        char *colon = (char *) strchr(cursor, ':');
-        if (colon) {
-            *colon = '\0';
-            nsAutoCString headerStr(cursor);
-            headerStr.CompressWhitespace();
-            *colon = ':';
-
-            nsAutoCString headerVal(colon + 1);
-            headerVal.CompressWhitespace();
+nsMultiMixedConv::ProcessHeader()
+{
+    mozilla::Tokenizer p(mResponseHeaderValue);
 
-            // examine header
-            if (headerStr.LowerCaseEqualsLiteral("content-type")) {
-                mContentType = headerVal;
-            } else if (headerStr.LowerCaseEqualsLiteral("content-length")) {
-                mContentLength = nsCRT::atoll(headerVal.get());
-            } else if (headerStr.LowerCaseEqualsLiteral("content-disposition")) {
-                mContentDisposition = headerVal;
-            } else if (headerStr.LowerCaseEqualsLiteral("set-cookie")) {
-                nsCOMPtr<nsIHttpChannelInternal> httpInternal =
-                    do_QueryInterface(aChannel);
-                if (httpInternal) {
-                    httpInternal->SetCookie(headerVal.get());
-                }
-            } else if (headerStr.LowerCaseEqualsLiteral("content-range") || 
-                       headerStr.LowerCaseEqualsLiteral("range") ) {
-                // something like: Content-range: bytes 7000-7999/8000
-                char* tmpPtr;
-
-                tmpPtr = (char *) strchr(colon + 1, '/');
-                if (tmpPtr) 
-                    *tmpPtr = '\0';
-
-                // pass the bytes-unit and the SP
-                char *range = (char *) strchr(colon + 2, ' ');
-                if (!range)
-                    return NS_ERROR_FAILURE;
-
-                do {
-                    range++;
-                } while (*range == ' ');
-
-                if (range[0] == '*'){
-                    mByteRangeStart = mByteRangeEnd = 0;
-                }
-                else {
-                    tmpPtr = (char *) strchr(range, '-');
-                    if (!tmpPtr)
-                        return NS_ERROR_FAILURE;
-                    
-                    tmpPtr[0] = '\0';
-                    
-                    mByteRangeStart = nsCRT::atoll(range);
-                    tmpPtr++;
-                    mByteRangeEnd = nsCRT::atoll(tmpPtr);
-                }
-
-                mIsByteRangeRequest = true;
-                if (mContentLength == UINT64_MAX)
-                    mContentLength = uint64_t(mByteRangeEnd - mByteRangeStart + 1);
-            }
-        }
-        *newLine = tmpChar;
-        newLine += lineFeedIncrement;
-        cursorLen -= (newLine - cursor);
-        cursor = newLine;
+    switch (mResponseHeader) {
+    case HEADER_CONTENT_TYPE:
+      mContentType = mResponseHeaderValue;
+      mContentType.CompressWhitespace();
+      break;
+    case HEADER_CONTENT_LENGTH:
+      p.SkipWhites();
+      if (!p.ReadInteger(&mContentLength)) {
+        return NS_ERROR_CORRUPTED_CONTENT;
+      }
+      break;
+    case HEADER_CONTENT_DISPOSITION:
+      mContentDisposition = mResponseHeaderValue;
+      mContentDisposition.CompressWhitespace();
+      break;
+    case HEADER_SET_COOKIE: {
+      nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(mChannel);
+      mResponseHeaderValue.CompressWhitespace();
+      if (httpInternal) {
+        httpInternal->SetCookie(mResponseHeaderValue.get());
+      }
+      break;
+    }
+    case HEADER_RANGE:
+    case HEADER_CONTENT_RANGE: {
+      if (!p.CheckWord("bytes") ||
+          !p.CheckWhite()) {
+        return NS_ERROR_CORRUPTED_CONTENT;
+      }
+      p.SkipWhites();
+      if (p.CheckChar('*')) {
+        mByteRangeStart = mByteRangeEnd = 0;
+      } else if (!p.ReadInteger(&mByteRangeStart) ||
+                 !p.CheckChar('-') ||
+                 !p.ReadInteger(&mByteRangeEnd)) {
+        return NS_ERROR_CORRUPTED_CONTENT;
+      }
+      mIsByteRangeRequest = true;
+      if (mContentLength == UINT64_MAX) {
+        mContentLength = uint64_t(mByteRangeEnd - mByteRangeStart + 1);
+      }
+      break;
+    }
+    case HEADER_UNKNOWN:
+      // We ignore anything else...
+      break;
     }
 
-    aPtr = cursor;
-    aLen = cursorLen;
-
-    *_retval = done;
-    return rv;
-}
-
-char *
-nsMultiMixedConv::FindToken(char *aCursor, uint32_t aLen) {
-    // strnstr without looking for null termination
-    const char *token = mToken.get();
-    char *cur = aCursor;
-
-    if (!(token && aCursor && *token)) {
-        NS_WARNING("bad data");
-        return nullptr;
-    }
-
-    for (; aLen >= mTokenLen; aCursor++, aLen--) {
-        if (!memcmp(aCursor, token, mTokenLen) ) {
-            if ((aCursor - cur) >= 2) {
-                // back the cursor up over a double dash for backwards compat.
-                if ((*(aCursor-1) == '-') && (*(aCursor-2) == '-')) {
-                    aCursor -= 2;
-                    aLen += 2;
-
-                    // we're playing w/ double dash tokens, adjust.
-                    mToken.Assign(aCursor, mTokenLen + 2);
-                    mTokenLen = mToken.Length();
-                }
-            }
-            return aCursor;
-        }
-    }
-
-    return nullptr;
+    return NS_OK;
 }
 
 nsresult
 NS_NewMultiMixedConv(nsMultiMixedConv** aMultiMixedConv)
 {
     NS_PRECONDITION(aMultiMixedConv != nullptr, "null ptr");
     if (! aMultiMixedConv)
         return NS_ERROR_NULL_POINTER;
 
     *aMultiMixedConv = new nsMultiMixedConv();
-    if (! *aMultiMixedConv)
-        return NS_ERROR_OUT_OF_MEMORY;
 
     NS_ADDREF(*aMultiMixedConv);
     return NS_OK;
 }
--- a/netwerk/streamconv/converters/nsMultiMixedConv.h
+++ b/netwerk/streamconv/converters/nsMultiMixedConv.h
@@ -9,16 +9,17 @@
 #include "nsIChannel.h"
 #include "nsString.h"
 #include "nsCOMPtr.h"
 #include "nsIByteRangeRequest.h"
 #include "nsILoadInfo.h"
 #include "nsIMultiPartChannel.h"
 #include "nsAutoPtr.h"
 #include "mozilla/Attributes.h"
+#include "mozilla/IncrementalTokenizer.h"
 #include "nsHttpResponseHead.h"
 
 #define NS_MULTIMIXEDCONVERTER_CID                         \
 { /* 7584CE90-5B25-11d3-A175-0050041CAF44 */         \
     0x7584ce90,                                      \
     0x5b25,                                          \
     0x11d3,                                          \
     {0xa1, 0x75, 0x0, 0x50, 0x4, 0x1c, 0xaf, 0x44}       \
@@ -112,65 +113,137 @@ protected:
 //  --BoundaryToken[\r]\n
 //  content-type: foo/bar[\r]\n
 //  ... (other headers if any)
 //  [\r]\n (second line feed to delimit end of headers)
 //  data
 //  --BoundaryToken-- (end delimited by final "--")
 //
 // linebreaks can be either CRLF or LFLF. linebreaks preceding
-// boundary tokens are considered part of the data. BoundaryToken
+// boundary tokens are NOT considered part of the data. BoundaryToken
 // is any opaque string.
 //  
 //
 
 class nsMultiMixedConv : public nsIStreamConverter {
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSISTREAMCONVERTER
     NS_DECL_NSISTREAMLISTENER
     NS_DECL_NSIREQUESTOBSERVER
 
-    nsMultiMixedConv();
+    explicit nsMultiMixedConv();
 
 protected:
+    typedef mozilla::IncrementalTokenizer::Token Token;
+
     virtual ~nsMultiMixedConv();
 
-    nsresult SendStart(nsIChannel *aChannel);
+    nsresult SendStart();
+    void AccumulateData(Token const & aToken);
+    nsresult SendData();
     nsresult SendStop(nsresult aStatus);
-    nsresult SendData(char *aBuffer, uint32_t aLen);
-    nsresult ParseHeaders(nsIChannel *aChannel, char *&aPtr,
-                          uint32_t &aLen, bool *_retval);
-    int32_t  PushOverLine(char *&aPtr, uint32_t &aLen);
-    char *FindToken(char *aCursor, uint32_t aLen);
-    nsresult BufferData(char *aData, uint32_t aLen);
 
     // member data
-    bool                mNewPart;        // Are we processing the beginning of a part?
-    bool                mProcessingHeaders;
     nsCOMPtr<nsIStreamListener> mFinalListener; // this guy gets the converted data via his OnDataAvailable()
 
-    nsCString           mToken;
-    uint32_t            mTokenLen;
-
+    nsCOMPtr<nsIChannel> mChannel; // The channel as we get in in OnStartRequest call
     RefPtr<nsPartChannel> mPartChannel;   // the channel for the given part we're processing.
                                         // one channel per part.
     nsCOMPtr<nsISupports> mContext;
     nsCString           mContentType;
     nsCString           mContentDisposition;
     uint64_t            mContentLength;
-    
-    char                *mBuffer;
-    uint32_t            mBufLen;
     uint64_t            mTotalSent;
-    bool                mFirstOnData;   // used to determine if we're in our first OnData callback.
 
     // The following members are for tracking the byte ranges in
     // multipart/mixed content which specified the 'Content-Range:'
     // header...
     int64_t             mByteRangeStart;
     int64_t             mByteRangeEnd;
     bool                mIsByteRangeRequest;
+    // This flag is set first time we create a part channel.
+    // We use it to prevent duplicated OnStopRequest call on the listener
+    // when we fail from some reason to ever create a part channel that
+    // ensures correct notifications.
+    bool                mRequestListenerNotified;
 
     uint32_t            mCurrentPartID;
+
+    // Flag preventing reenter of OnDataAvailable in case the target listener
+    // ends up spinning the event loop.
+    bool                mInOnDataAvailable;
+
+    // Current state of the incremental parser
+    enum EParserState {
+      BOUNDARY,
+      BOUNDARY_CRLF,
+      HEADER_NAME,
+      HEADER_SEP,
+      HEADER_VALUE,
+      BODY_INIT,
+      BODY,
+      TRAIL_DASH1,
+      TRAIL_DASH2,
+      EPILOGUE,
+
+      INIT = BOUNDARY
+    } mParserState;
+
+    // Response part header value, valid when we find a header name
+    // we recognize.
+    enum EHeader : uint32_t {
+      HEADER_FIRST,
+      HEADER_CONTENT_TYPE = HEADER_FIRST,
+      HEADER_CONTENT_LENGTH,
+      HEADER_CONTENT_DISPOSITION,
+      HEADER_SET_COOKIE,
+      HEADER_CONTENT_RANGE,
+      HEADER_RANGE,
+      HEADER_UNKNOWN
+    } mResponseHeader;
+    // Cumulated value of a response header.
+    nsCString mResponseHeaderValue;
+
+    nsCString mBoundary;
+    mozilla::IncrementalTokenizer mTokenizer;
+
+    // When in the "body parsing" mode, see below, we cumulate raw data
+    // incrementally to mainly avoid any unnecessary granularity.
+    // mRawData points to the first byte in the tokenizer buffer where part
+    // body data begins or continues.  mRawDataLength is a cumulated length
+    // of that data during a single tokenizer input feed.  This is always
+    // flushed right after we fed the tokenizer.
+    nsACString::const_char_iterator mRawData;
+    nsACString::size_type mRawDataLength;
+
+    // At the start we don't know if the server will be sending boundary with
+    // or without the leading dashes.
+    Token mBoundaryToken;
+    Token mBoundaryTokenWithDashes;
+    // We need these custom tokens to allow finding CRLF when in the binary mode.
+    // CRLF before boundary is considered part of the boundary and not part of
+    // the data.
+    Token mLFToken;
+    Token mCRLFToken;
+    // Custom tokens for each of the response headers we recognize.
+    Token mHeaderTokens[HEADER_UNKNOWN];
+
+    // Resets values driven by part headers, like content type, to their defaults,
+    // called at the start of every part processing.
+    void HeadersToDefault();
+    // Processes captured value of mResponseHeader header.
+    nsresult ProcessHeader();
+    // Switches the parser and tokenizer state to "binary mode" which only searches
+    // for the 'CRLF boundary' delimiter.
+    void SwitchToBodyParsing();
+    // Switches to the default mode, we are in this mode when parsing headers and
+    // control data around the boundary delimiters.
+    void SwitchToControlParsing();
+    // Turns on or off recognition of the headers we recognize in part heads.
+    void SetHeaderTokensEnabled(bool aEnable);
+
+    // The main parser callback called by the IncrementalTokenizer
+    // instance from OnDataAvailable or OnStopRequest.
+    nsresult ConsumeToken(Token const & token);
 };
 
 #endif /* __nsmultimixedconv__h__ */
copy from netwerk/test/unit/test_multipart_streamconv.js
copy to netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js
--- a/netwerk/test/unit/test_multipart_streamconv.js
+++ b/netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js
@@ -6,40 +6,57 @@ var httpserver = null;
 XPCOMUtils.defineLazyGetter(this, "uri", function() {
   return "http://localhost:" + httpserver.identity.primaryPort + "/multipart";
 });
 
 function make_channel(url) {
   return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true});
 }
 
-var multipartBody = "--boundary\r\n\r\nSome text\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--";
+var multipartBody = "--boundary\r\n\r\nSome text\r\n--boundary\r\nContent-Type: text/x-test\r\n\r\n<?xml version='1.1'?>\r\n<root/>\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--";
 
 function contentHandler(metadata, response)
 {
   response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"');
-  response.bodyOutputStream.write(multipartBody, multipartBody.length);
+  response.processAsync();
+
+  var body = multipartBody;
+  function byteByByte()
+  {
+    if (!body.length) {
+      response.finish();
+      return;
+    }
+
+    var onebyte = body[0];
+    response.bodyOutputStream.write(onebyte, 1);
+    body = body.substring(1);
+    do_timeout(1, byteByByte);
+  }
+
+  do_timeout(1, byteByByte);
 }
 
 var numTests = 2;
 var testNum = 0;
 
 var testData =
   [
    { data: "Some text", type: "text/plain" },
+   { data: "<?xml version='1.1'?>\r\n<root/>", type: "text/x-test" },
    { data: "<?xml version='1.0'?><root/>", type: "text/xml" }
   ];
 
 function responseHandler(request, buffer)
 {
-    do_check_eq(buffer, testData[testNum].data);
-    do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType,
-		testData[testNum].type);
-    if (++testNum == numTests)
-	httpserver.stop(do_test_finished);
+  do_check_eq(buffer, testData[testNum].data);
+  do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType,
+  testData[testNum].type);
+  if (++testNum == numTests)
+    httpserver.stop(do_test_finished);
 }
 
 var multipartListener = {
   _buffer: "",
   _index: 0,
 
   QueryInterface: function(iid) {
     if (iid.equals(Components.interfaces.nsIStreamListener) ||
@@ -60,17 +77,17 @@ var multipartListener = {
     } catch (ex) {
       do_throw("Error in onDataAvailable: " + ex);
     }
   },
 
   onStopRequest: function(request, context, status) {
     this._index++;
     // Second part should be last part
-    do_check_eq(request.QueryInterface(Ci.nsIMultiPartChannel).isLastPart, this._index == 2);
+    do_check_eq(request.QueryInterface(Ci.nsIMultiPartChannel).isLastPart, this._index == testData.length);
     try {
       responseHandler(request, this._buffer);
     } catch (ex) {
       do_throw("Error in closure function: " + ex);
     }
   }
 };
 
--- a/netwerk/test/unit/test_multipart_streamconv.js
+++ b/netwerk/test/unit/test_multipart_streamconv.js
@@ -6,40 +6,41 @@ var httpserver = null;
 XPCOMUtils.defineLazyGetter(this, "uri", function() {
   return "http://localhost:" + httpserver.identity.primaryPort + "/multipart";
 });
 
 function make_channel(url) {
   return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true});
 }
 
-var multipartBody = "--boundary\r\n\r\nSome text\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--";
+var multipartBody = "--boundary\r\n\r\nSome text\r\n--boundary\r\nContent-Type: text/x-test\r\n\r\n<?xml version='1.1'?>\r\n<root/>\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--";
 
 function contentHandler(metadata, response)
 {
   response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"');
   response.bodyOutputStream.write(multipartBody, multipartBody.length);
 }
 
 var numTests = 2;
 var testNum = 0;
 
 var testData =
   [
    { data: "Some text", type: "text/plain" },
+   { data: "<?xml version='1.1'?>\r\n<root/>", type: "text/x-test" },
    { data: "<?xml version='1.0'?><root/>", type: "text/xml" }
   ];
 
 function responseHandler(request, buffer)
 {
-    do_check_eq(buffer, testData[testNum].data);
-    do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType,
-		testData[testNum].type);
-    if (++testNum == numTests)
-	httpserver.stop(do_test_finished);
+  do_check_eq(buffer, testData[testNum].data);
+  do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType,
+  testData[testNum].type);
+  if (++testNum == numTests)
+    httpserver.stop(do_test_finished);
 }
 
 var multipartListener = {
   _buffer: "",
   _index: 0,
 
   QueryInterface: function(iid) {
     if (iid.equals(Components.interfaces.nsIStreamListener) ||
@@ -60,17 +61,17 @@ var multipartListener = {
     } catch (ex) {
       do_throw("Error in onDataAvailable: " + ex);
     }
   },
 
   onStopRequest: function(request, context, status) {
     this._index++;
     // Second part should be last part
-    do_check_eq(request.QueryInterface(Ci.nsIMultiPartChannel).isLastPart, this._index == 2);
+    do_check_eq(request.QueryInterface(Ci.nsIMultiPartChannel).isLastPart, this._index == testData.length);
     try {
       responseHandler(request, this._buffer);
     } catch (ex) {
       do_throw("Error in closure function: " + ex);
     }
   }
 };
 
copy from netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js
copy to netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js
--- a/netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js
+++ b/netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js
@@ -6,17 +6,17 @@ var httpserver = null;
 XPCOMUtils.defineLazyGetter(this, "uri", function() {
   return "http://localhost:" + httpserver.identity.primaryPort + "/multipart";
 });
 
 function make_channel(url) {
   return NetUtil.newChannel({uri: url, loadUsingSystemPrincipal: true});
 }
 
-var multipartBody = "\r\nSome text\r\n--boundary\r\n\r\n<?xml version='1.0'?><root/>\r\n--boundary--";
+var multipartBody = "\r\nSome text\r\nboundary\r\n\r\n<?xml version='1.0'?><root/>\r\nboundary--";
 
 function contentHandler(metadata, response)
 {
   response.setHeader("Content-Type", 'multipart/mixed; boundary="boundary"');
   response.bodyOutputStream.write(multipartBody, multipartBody.length);
 }
 
 var numTests = 2;
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -237,16 +237,18 @@ run-sequentially = node server exception
 [test_localstreams.js]
 [test_large_port.js]
 [test_mismatch_last-modified.js]
 [test_MIME_params.js]
 [test_mozTXTToHTMLConv.js]
 [test_multipart_byteranges.js]
 [test_multipart_streamconv.js]
 [test_multipart_streamconv_missing_lead_boundary.js]
+[test_multipart_streamconv_missing_boundary_lead_dashes.js]
+[test_multipart_streamconv-byte-by-byte.js]
 [test_nestedabout_serialize.js]
 [test_net_addr.js]
 # Bug 732363: test fails on windows for unknown reasons.
 skip-if = os == "win"
 [test_nojsredir.js]
 [test_offline_status.js]
 [test_original_sent_received_head.js]
 [test_parse_content_type.js]