Bug 1322373 - TLS 1.3 early-data for HTTP/2. r=dragana,mcmanus
authorPatrick McManus <mcmanus@ducksong.com>
Tue, 21 Feb 2017 15:19:46 -0500
changeset 373204 001bc0207dbcf4b008d9a23030e9b3adc5bdd7ab
parent 373203 c35afe490583c88e2c6a7041aad3d1451b3e82bc
child 373205 19f1d4210d56f8c164cc02baeec2e42ca2ef792d
push id10863
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 23:02:23 +0000
treeherdermozilla-aurora@0931190cd725 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdragana, mcmanus
bugs1322373
milestone54.0a1
Bug 1322373 - TLS 1.3 early-data for HTTP/2. r=dragana,mcmanus
netwerk/protocol/http/ASpdySession.cpp
netwerk/protocol/http/ASpdySession.h
netwerk/protocol/http/Http2Session.cpp
netwerk/protocol/http/Http2Session.h
netwerk/protocol/http/Http2Stream.cpp
netwerk/protocol/http/Http2Stream.h
netwerk/protocol/http/nsAHttpTransaction.h
netwerk/protocol/http/nsHttpConnection.cpp
netwerk/protocol/http/nsHttpConnection.h
netwerk/protocol/http/nsHttpTransaction.cpp
netwerk/protocol/http/nsHttpTransaction.h
--- a/netwerk/protocol/http/ASpdySession.cpp
+++ b/netwerk/protocol/http/ASpdySession.cpp
@@ -27,31 +27,32 @@ namespace net {
 ASpdySession::ASpdySession()
 {
 }
 
 ASpdySession::~ASpdySession() = default;
 
 ASpdySession *
 ASpdySession::NewSpdySession(uint32_t version,
-                             nsISocketTransport *aTransport)
+                             nsISocketTransport *aTransport,
+                             bool attemptingEarlyData)
 {
   // This is a necko only interface, so we can enforce version
   // requests as a precondition
   MOZ_ASSERT(version == HTTP_VERSION_2,
              "Unsupported spdy version");
 
   // Don't do a runtime check of IsSpdyV?Enabled() here because pref value
   // may have changed since starting negotiation. The selected protocol comes
   // from a list provided in the SERVER HELLO filtered by our acceptable
   // versions, so there is no risk of the server ignoring our prefs.
 
   Telemetry::Accumulate(Telemetry::SPDY_VERSION2, version);
 
-  return new Http2Session(aTransport, version);
+  return new Http2Session(aTransport, version, attemptingEarlyData);
 }
 
 SpdyInformation::SpdyInformation()
 {
   // highest index of enabled protocols is the
   // most preferred for ALPN negotiaton
   Version[0] = HTTP_VERSION_2;
   VersionString[0] = NS_LITERAL_CSTRING("h2");
--- a/netwerk/protocol/http/ASpdySession.h
+++ b/netwerk/protocol/http/ASpdySession.h
@@ -23,18 +23,19 @@ public:
 
   virtual bool AddStream(nsAHttpTransaction *, int32_t,
                          bool, nsIInterfaceRequestor *) = 0;
   virtual bool CanReuse() = 0;
   virtual bool RoomForMoreStreams() = 0;
   virtual PRIntervalTime IdleTime() = 0;
   virtual uint32_t ReadTimeoutTick(PRIntervalTime now) = 0;
   virtual void DontReuse() = 0;
+  virtual uint32_t SpdyVersion() = 0;
 
-  static ASpdySession *NewSpdySession(uint32_t version, nsISocketTransport *);
+  static ASpdySession *NewSpdySession(uint32_t version, nsISocketTransport *, bool);
 
   // MaybeReTunnel() is called by the connection manager when it cannot
   // dispatch a tunneled transaction. That might be because the tunnels it
   // expects to see are dead (and we may or may not be able to make more),
   // or it might just need to wait longer for one of them to become free.
   //
   // return true if the session takes back ownership of the transaction from
   // the connection manager.
--- a/netwerk/protocol/http/Http2Session.cpp
+++ b/netwerk/protocol/http/Http2Session.cpp
@@ -59,17 +59,17 @@ const uint8_t Http2Session::kMagicHello[
 };
 
 #define RETURN_SESSION_ERROR(o,x)  \
 do {                             \
   (o)->mGoAwayReason = (x);      \
   return NS_ERROR_ILLEGAL_VALUE; \
   } while (0)
 
-Http2Session::Http2Session(nsISocketTransport *aSocketTransport, uint32_t version)
+Http2Session::Http2Session(nsISocketTransport *aSocketTransport, uint32_t version, bool attemptingEarlyData)
   : mSocketTransport(aSocketTransport)
   , mSegmentReader(nullptr)
   , mSegmentWriter(nullptr)
   , mNextStreamID(3) // 1 is reserved for Updgrade handshakes
   , mLastPushedID(0)
   , mConcurrentHighWater(0)
   , mDownstreamState(BUFFERING_OPENING_SETTINGS)
   , mInputFrameBufferSize(kDefaultBufferSize)
@@ -107,16 +107,17 @@ Http2Session::Http2Session(nsISocketTran
   , mOutputQueueUsed(0)
   , mOutputQueueSent(0)
   , mLastReadEpoch(PR_IntervalNow())
   , mPingSentEpoch(0)
   , mPreviousUsed(false)
   , mWaitingForSettingsAck(false)
   , mGoAwayOnPush(false)
   , mUseH2Deps(false)
+  , mAttemptingEarlyData(attemptingEarlyData)
 {
   MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
 
   static uint64_t sSerial;
   mSerial = ++sSerial;
 
   LOG3(("Http2Session::Http2Session %p serial=0x%" PRIX64 "\n", this, mSerial));
 
@@ -496,16 +497,22 @@ Http2Session::SetWriteCallbacks()
 {
   if (mConnection && (GetWriteQueueSize() || mOutputQueueUsed))
     mConnection->ResumeSend();
 }
 
 void
 Http2Session::RealignOutputQueue()
 {
+  if (mAttemptingEarlyData) {
+    // We can't realign right now, because we may need what's in there if early
+    // data fails.
+    return;
+  }
+
   mOutputQueueUsed -= mOutputQueueSent;
   memmove(mOutputQueueBuffer.get(),
           mOutputQueueBuffer.get() + mOutputQueueSent,
           mOutputQueueUsed);
   mOutputQueueSent = 0;
 }
 
 void
@@ -513,34 +520,46 @@ Http2Session::FlushOutputQueue()
 {
   if (!mSegmentReader || !mOutputQueueUsed)
     return;
 
   nsresult rv;
   uint32_t countRead;
   uint32_t avail = mOutputQueueUsed - mOutputQueueSent;
 
+  if (!avail && mAttemptingEarlyData) {
+    // This is kind of a hack, but there are cases where we'll have already
+    // written the data we want whlie doing early data, but we get called again
+    // with a reader, and we need to avoid calling the reader when there's
+    // nothing for it to read.
+    return;
+  }
+
   rv = mSegmentReader->
     OnReadSegment(mOutputQueueBuffer.get() + mOutputQueueSent, avail,
                   &countRead);
   LOG3(("Http2Session::FlushOutputQueue %p sz=%d rv=%" PRIx32 " actual=%d",
         this, avail, static_cast<uint32_t>(rv), countRead));
 
   // Dont worry about errors on write, we will pick this up as a read error too
   if (NS_FAILED(rv))
     return;
 
+  mOutputQueueSent += countRead;
+
+  if (mAttemptingEarlyData) {
+    return;
+  }
+
   if (countRead == avail) {
     mOutputQueueUsed = 0;
     mOutputQueueSent = 0;
     return;
   }
 
-  mOutputQueueSent += countRead;
-
   // If the output queue is close to filling up and we have sent out a good
   // chunk of data from the beginning then realign it.
 
   if ((mOutputQueueSent >= kQueueMinimumCleanup) &&
       ((mOutputQueueSize - mOutputQueueUsed) < kQueueTailRoom)) {
     RealignOutputQueue();
   }
 }
@@ -550,16 +569,22 @@ Http2Session::DontReuse()
 {
   LOG3(("Http2Session::DontReuse %p\n", this));
   mShouldGoAway = true;
   if (!mStreamTransactionHash.Count())
     Close(NS_OK);
 }
 
 uint32_t
+Http2Session::SpdyVersion()
+{
+  return HTTP_VERSION_2;
+}
+
+uint32_t
 Http2Session::GetWriteQueueSize()
 {
   MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
 
   return mReadyForWrite.GetSize();
 }
 
 void
@@ -2339,25 +2364,61 @@ Http2Session::ReadSegmentsAgain(nsAHttpS
   LOG3(("Http2Session::ReadSegments %p", this));
 
   Http2Stream *stream = static_cast<Http2Stream *>(mReadyForWrite.PopFront());
   if (!stream) {
     LOG3(("Http2Session %p could not identify a stream to write; suspending.",
           this));
     FlushOutputQueue();
     SetWriteCallbacks();
-    return NS_BASE_STREAM_WOULD_BLOCK;
+    if (mAttemptingEarlyData) {
+      // We can still try to send our preamble as early-data
+      *countRead = mOutputQueueUsed - mOutputQueueSent;
+    }
+    return *countRead ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK;
+  }
+
+  uint32_t earlyDataUsed = 0;
+  if (mAttemptingEarlyData) {
+    if (!stream->Do0RTT()) {
+      LOG3(("Http2Session %p will not get early data from Http2Stream %p 0x%X",
+            this, stream, stream->StreamID()));
+      FlushOutputQueue();
+      SetWriteCallbacks();
+      // We can still send our preamble
+      *countRead = mOutputQueueUsed - mOutputQueueSent;
+      return *countRead ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK;
+    }
+
+    if (!m0RTTStreams.Contains(stream->StreamID())) {
+      m0RTTStreams.AppendElement(stream->StreamID());
+    }
+
+    // Need to adjust this to only take as much as we can fit in with the
+    // preamble/settings/priority stuff
+    count -= (mOutputQueueUsed - mOutputQueueSent);
+
+    // Keep track of this to add it into countRead later, as
+    // stream->ReadSegments will likely change the value of mOutputQueueUsed.
+    earlyDataUsed = mOutputQueueUsed - mOutputQueueSent;
   }
 
   LOG3(("Http2Session %p will write from Http2Stream %p 0x%X "
         "block-input=%d block-output=%d\n", this, stream, stream->StreamID(),
         stream->RequestBlockedOnRead(), stream->BlockedOnRwin()));
 
   rv = stream->ReadSegments(this, count, countRead);
 
+  if (earlyDataUsed) {
+    // Do this here because countRead could get reset somewhere down the rabbit
+    // hole of stream->ReadSegments, and we want to make sure we return the
+    // proper value to our caller.
+    *countRead += earlyDataUsed;
+  }
+
   // Not every permutation of stream->ReadSegents produces data (and therefore
   // tries to flush the output queue) - SENDING_FIN_STREAM can be an example
   // of that. But we might still have old data buffered that would be good
   // to flush.
   FlushOutputQueue();
 
   // Allow new server reads - that might be data or control information
   // (e.g. window updates or http replies) that are responses to these writes
@@ -2899,16 +2960,68 @@ nsresult
 Http2Session::WriteSegments(nsAHttpSegmentWriter *writer,
                             uint32_t count, uint32_t *countWritten)
 {
   bool again = false;
   return WriteSegmentsAgain(writer, count, countWritten, &again);
 }
 
 nsresult
+Http2Session::Finish0RTT(bool aRestart, bool aAlpnChanged)
+{
+  MOZ_ASSERT(mAttemptingEarlyData);
+  LOG3(("Http2Session::Finish0RTT %p aRestart=%d aAlpnChanged=%d", this,
+        aRestart, aAlpnChanged));
+
+  for (size_t i = 0; i < m0RTTStreams.Length(); ++i) {
+    // Instead of passing (aRestart, aAlpnChanged) here, we use aAlpnChanged for
+    // both arguments because as long as the alpn token stayed the same, we can
+    // just reuse what we have in our buffer to send instead of having to have
+    // the transaction rewind and read it all over again. We only need to rewind
+    // the transaction if we're switching to a new protocol, because our buffer
+    // won't get used in that case.
+    Http2Stream *stream = mStreamIDHash.Get(m0RTTStreams[i]);
+    if (stream) {
+      stream->Finish0RTT(aAlpnChanged, aAlpnChanged);
+    }
+  }
+
+  if (aRestart) {
+    // 0RTT failed
+    if (aAlpnChanged) {
+      // This is a slightly more involved case - we need to get all our streams/
+      // transactions back in the queue so they can restart as http/1
+
+      // These must be set this way to ensure we gracefully restart all streams
+      mGoAwayID = 0;
+      mCleanShutdown = true;
+
+      // Close takes care of the rest of our work for us. The reason code here
+      // doesn't matter, as we aren't actually going to send a GOAWAY frame, but
+      // we use NS_ERROR_NET_RESET as it's closest to the truth.
+      Close(NS_ERROR_NET_RESET);
+    } else {
+      // This is the easy case - early data failed, but we're speaking h2, so
+      // we just need to rewind to the beginning of the preamble and try again.
+      mOutputQueueSent = 0;
+    }
+  } else {
+    // 0RTT succeeded
+    // Make sure we look for any incoming data in repsonse to our early data.
+    ResumeRecv();
+  }
+
+  mAttemptingEarlyData = false;
+  m0RTTStreams.Clear();
+  RealignOutputQueue();
+
+  return NS_OK;
+}
+
+nsresult
 Http2Session::ProcessConnectedPush(Http2Stream *pushConnectedStream,
                                    nsAHttpSegmentWriter * writer,
                                    uint32_t count, uint32_t *countWritten)
 {
   LOG3(("Http2Session::ProcessConnectedPush %p 0x%X\n",
         this, pushConnectedStream->StreamID()));
   mSegmentWriter = writer;
   nsresult rv = pushConnectedStream->WriteSegments(this, count, countWritten);
@@ -3106,17 +3219,19 @@ Http2Session::Close(nsresult aReason)
     goAwayReason = mGoAwayReason;
   } else if (NS_SUCCEEDED(aReason)) {
     goAwayReason = NO_HTTP_ERROR;
   } else if (aReason == NS_ERROR_ILLEGAL_VALUE) {
     goAwayReason = PROTOCOL_ERROR;
   } else {
     goAwayReason = INTERNAL_ERROR;
   }
-  GenerateGoAway(goAwayReason);
+  if (!mAttemptingEarlyData) {
+    GenerateGoAway(goAwayReason);
+  }
   mConnection = nullptr;
   mSegmentReader = nullptr;
   mSegmentWriter = nullptr;
 }
 
 nsHttpConnectionInfo *
 Http2Session::ConnectionInfo()
 {
@@ -3208,17 +3323,17 @@ Http2Session::OnReadSegment(const char *
   FlushOutputQueue();
 
   return NS_OK;
 }
 
 nsresult
 Http2Session::CommitToSegmentSize(uint32_t count, bool forceCommitment)
 {
-  if (mOutputQueueUsed)
+  if (mOutputQueueUsed && !mAttemptingEarlyData)
     FlushOutputQueue();
 
   // would there be enough room to buffer this if needed?
   if ((mOutputQueueUsed + count) <= (mOutputQueueSize - kQueueReserved))
     return NS_OK;
 
   // if we are using part of our buffers already, try again later unless
   // forceCommitment is set.
@@ -3528,22 +3643,28 @@ Http2Session::ALPNCallback(nsISupports *
     }
   }
   return false;
 }
 
 nsresult
 Http2Session::ConfirmTLSProfile()
 {
-  if (mTLSProfileConfirmed)
+  if (mTLSProfileConfirmed) {
     return NS_OK;
+  }
 
   LOG3(("Http2Session::ConfirmTLSProfile %p mConnection=%p\n",
         this, mConnection.get()));
 
+  if (mAttemptingEarlyData) {
+    LOG3(("Http2Session::ConfirmTLSProfile %p temporarily passing due to early data\n", this));
+    return NS_OK;
+  }
+
   if (!gHttpHandler->EnforceHttp2TlsProfile()) {
     LOG3(("Http2Session::ConfirmTLSProfile %p passed due to configuration bypass\n", this));
     mTLSProfileConfirmed = true;
     return NS_OK;
   }
 
   if (!mConnection)
     return NS_ERROR_FAILURE;
--- a/netwerk/protocol/http/Http2Session.h
+++ b/netwerk/protocol/http/Http2Session.h
@@ -38,22 +38,23 @@ class Http2Session final : public ASpdyS
 
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSAHTTPTRANSACTION
   NS_DECL_NSAHTTPCONNECTION(mConnection)
   NS_DECL_NSAHTTPSEGMENTREADER
   NS_DECL_NSAHTTPSEGMENTWRITER
 
- Http2Session(nsISocketTransport *, uint32_t version);
+ Http2Session(nsISocketTransport *, uint32_t version, bool attemptingEarlyData);
 
   bool AddStream(nsAHttpTransaction *, int32_t,
                  bool, nsIInterfaceRequestor *) override;
   bool CanReuse() override { return !mShouldGoAway && !mClosed; }
   bool RoomForMoreStreams() override;
+  uint32_t SpdyVersion() override;
 
   // When the connection is active this is called up to once every 1 second
   // return the interval (in seconds) that the connection next wants to
   // have this invoked. It might happen sooner depending on the needs of
   // other connections.
   uint32_t  ReadTimeoutTick(PRIntervalTime now) override;
 
   // Idle time represents time since "goodput".. e.g. a data or header frame
@@ -230,16 +231,18 @@ public:
 
   void SendPing() override;
   bool MaybeReTunnel(nsAHttpTransaction *) override;
   bool UseH2Deps() { return mUseH2Deps; }
 
   // overload of nsAHttpTransaction
   nsresult ReadSegmentsAgain(nsAHttpSegmentReader *, uint32_t, uint32_t *, bool *) override final;
   nsresult WriteSegmentsAgain(nsAHttpSegmentWriter *, uint32_t , uint32_t *, bool *) override final;
+  bool Do0RTT() override final { return true; }
+  nsresult Finish0RTT(bool aRestart, bool aAlpnChanged) override final;
 
 private:
 
   // These internal states do not correspond to the states of the HTTP/2 specification
   enum internalStateType {
     BUFFERING_OPENING_SETTINGS,
     BUFFERING_FRAME_HEADER,
     BUFFERING_CONTROL_FRAME,
@@ -490,16 +493,20 @@ private:
   // receive a PUSH_PROMISE, but we have to wait for the SETTINGS ACK before
   // we can actually tell the other end to go away. These help us keep track
   // of that state so we can behave appropriately.
   bool mWaitingForSettingsAck;
   bool mGoAwayOnPush;
 
   bool mUseH2Deps;
 
+  bool mAttemptingEarlyData;
+  // The ID(s) of the stream(s) that we are getting 0RTT data from.
+  nsTArray<uint32_t> m0RTTStreams;
+
 private:
 /// connect tunnels
   void DispatchOnTunnel(nsAHttpTransaction *, nsIInterfaceRequestor *);
   void CreateTunnel(nsHttpTransaction *, nsHttpConnectionInfo *, nsIInterfaceRequestor *);
   void RegisterTunnel(Http2Stream *);
   void UnRegisterTunnel(Http2Stream *);
   uint32_t FindTunnelCount(nsHttpConnectionInfo *);
   nsDataHashtable<nsCStringHashKey, uint32_t> mTunnelHash;
--- a/netwerk/protocol/http/Http2Stream.cpp
+++ b/netwerk/protocol/http/Http2Stream.cpp
@@ -65,16 +65,17 @@ Http2Stream::Http2Stream(nsAHttpTransact
   , mTxInlineFrameUsed(0)
   , mTxStreamFrameSize(0)
   , mRequestBodyLenRemaining(0)
   , mLocalUnacked(0)
   , mBlockedOnRwin(false)
   , mTotalSent(0)
   , mTotalRead(0)
   , mPushSource(nullptr)
+  , mAttempting0RTT(false)
   , mIsTunnel(false)
   , mPlainTextTunnel(false)
 {
   MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
 
   LOG3(("Http2Stream::Http2Stream %p", this));
 
   mServerReceiveWindow = session->GetServerInitialStreamWindow();
@@ -921,17 +922,19 @@ Http2Stream::TransmitFrame(const char *b
                "inconsistent stream commitment count");
 
     Http2Session::LogIO(mSession, this, "Writing from Transaction Buffer",
                          buf, transmittedCount);
 
     *countUsed += mTxStreamFrameSize;
   }
 
-  mSession->FlushOutputQueue();
+  if (!mAttempting0RTT) {
+    mSession->FlushOutputQueue();
+  }
 
   // calling this will trigger waiting_for if mRequestBodyLenRemaining is 0
   UpdateTransportSendEvents(mTxInlineFrameUsed + mTxStreamFrameSize);
 
   mTxInlineFrameUsed = 0;
   mTxStreamFrameSize = 0;
 
   return NS_OK;
@@ -1464,10 +1467,31 @@ void
 Http2Stream::MapStreamToHttpConnection()
 {
   RefPtr<SpdyConnectTransaction> qiTrans(mTransaction->QuerySpdyConnectTransaction());
   MOZ_ASSERT(qiTrans);
   qiTrans->MapStreamToHttpConnection(mSocketTransport,
                                      mTransaction->ConnectionInfo());
 }
 
+// -----------------------------------------------------------------------------
+// mirror nsAHttpTransaction
+// -----------------------------------------------------------------------------
+
+bool
+Http2Stream::Do0RTT()
+{
+  MOZ_ASSERT(mTransaction);
+  mAttempting0RTT = true;
+  return mTransaction->Do0RTT();
+}
+
+nsresult
+Http2Stream::Finish0RTT(bool aRestart, bool aAlpnChanged)
+{
+  MOZ_ASSERT(mTransaction);
+  mAttempting0RTT = false;
+  return mTransaction->Finish0RTT(aRestart, aAlpnChanged);
+}
+
+
 } // namespace net
 } // namespace mozilla
--- a/netwerk/protocol/http/Http2Stream.h
+++ b/netwerk/protocol/http/Http2Stream.h
@@ -149,16 +149,20 @@ public:
 
   static nsresult MakeOriginURL(const nsACString &origin,
                                 RefPtr<nsStandardURL> &url);
 
   static nsresult MakeOriginURL(const nsACString &scheme,
                                 const nsACString &origin,
                                 RefPtr<nsStandardURL> &url);
 
+  // Mirrors nsAHttpTransaction
+  bool Do0RTT();
+  nsresult Finish0RTT(bool aRestart, bool aAlpnIgnored);
+
 protected:
   static void CreatePushHashKey(const nsCString &scheme,
                                 const nsCString &hostHeader,
                                 uint64_t serial,
                                 const nsCSubstring &pathInfo,
                                 nsCString &outOrigin,
                                 nsCString &outKey);
 
@@ -323,16 +327,18 @@ private:
 
   // For Http2Push
   Http2PushedStream *mPushSource;
 
   // Used to store stream data when the transaction channel cannot keep up
   // and flow control has not yet kicked in.
   SimpleBuffer mSimpleBuffer;
 
+  bool mAttempting0RTT;
+
 /// connect tunnels
 public:
   bool IsTunnel() { return mIsTunnel; }
 private:
   void ClearTransactionsBlockedOnTunnel();
   void MapStreamToPlainText();
   void MapStreamToHttpConnection();
 
--- a/netwerk/protocol/http/nsAHttpTransaction.h
+++ b/netwerk/protocol/http/nsAHttpTransaction.h
@@ -211,18 +211,21 @@ public:
     }
     // This function will be called when a tls handshake has been finished and
     // we know whether early-data that was sent has been accepted or not, e.g.
     // do we need to restart a transaction. This will be called only if Do0RTT
     // returns true.
     // If aRestart parameter is true we need to restart the transaction,
     // otherwise the erly-data has been accepted and we can continue the
     // transaction.
+    // If aAlpnChanged is true (and we were assuming http/2), we'll need to take
+    // the transactions out of the session, rewind them all, and start them back
+    // over as http/1 transactions
     // The function will return success or failure of the transaction restart.
-    virtual nsresult Finish0RTT(bool aRestart) {
+    virtual nsresult Finish0RTT(bool aRestart, bool aAlpnChanged) {
         return NS_ERROR_NOT_IMPLEMENTED;
     }
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsAHttpTransaction, NS_AHTTPTRANSACTION_IID)
 
 #define NS_DECL_NSAHTTPTRANSACTION \
     void SetConnection(nsAHttpConnection *) override; \
--- a/netwerk/protocol/http/nsHttpConnection.cpp
+++ b/netwerk/protocol/http/nsHttpConnection.cpp
@@ -82,16 +82,17 @@ nsHttpConnection::nsHttpConnection()
     , mTransactionCaps(0)
     , mResponseTimeoutEnabled(false)
     , mTCPKeepaliveConfig(kTCPKeepaliveDisabled)
     , mForceSendPending(false)
     , m0RTTChecked(false)
     , mWaitingFor0RTTResponse(false)
     , mContentBytesWritten0RTT(0)
     , mEarlyDataNegotiated(false)
+    , mDid0RTTSpdy(false)
 {
     LOG(("Creating nsHttpConnection @%p\n", this));
 
     // the default timeout is for when this connection has not yet processed a
     // transaction
     static const PRIntervalTime k5Sec = PR_SecondsToInterval(5);
     mIdleTimeout =
         (k5Sec < gHttpHandler->IdleTimeout()) ? k5Sec : gHttpHandler->IdleTimeout();
@@ -153,26 +154,123 @@ nsHttpConnection::Init(nsHttpConnectionI
     mCallbacks = new nsMainThreadPtrHolder<nsIInterfaceRequestor>(callbacks, false);
 
     mSocketTransport->SetEventSink(this, nullptr);
     mSocketTransport->SetSecurityCallbacks(this);
 
     return NS_OK;
 }
 
+nsresult
+nsHttpConnection::TryTakeSubTransactions(nsTArray<RefPtr<nsAHttpTransaction> > &list)
+{
+    nsresult rv = mTransaction->TakeSubTransactions(list);
+
+    if (rv == NS_ERROR_ALREADY_OPENED) {
+        // Has the interface for TakeSubTransactions() changed?
+        LOG(("TakeSubTransactions somehow called after "
+             "nsAHttpTransaction began processing\n"));
+        MOZ_ASSERT(false,
+                   "TakeSubTransactions somehow called after "
+                   "nsAHttpTransaction began processing");
+        mTransaction->Close(NS_ERROR_ABORT);
+        return rv;
+    }
+
+    if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) {
+        // Has the interface for TakeSubTransactions() changed?
+        LOG(("unexpected rv from nnsAHttpTransaction::TakeSubTransactions()"));
+        MOZ_ASSERT(false,
+                   "unexpected result from "
+                   "nsAHttpTransaction::TakeSubTransactions()");
+        mTransaction->Close(NS_ERROR_ABORT);
+        return rv;
+    }
+
+    return rv;
+}
+
+nsresult
+nsHttpConnection::MoveTransactionsToSpdy(nsresult status, nsTArray<RefPtr<nsAHttpTransaction> > &list)
+{
+    if (NS_FAILED(status)) { // includes NS_ERROR_NOT_IMPLEMENTED
+        MOZ_ASSERT(list.IsEmpty(), "sub transaction list not empty");
+
+        // This is ok - treat mTransaction as a single real request.
+        // Wrap the old http transaction into the new spdy session
+        // as the first stream.
+        LOG(("nsHttpConnection::MoveTransactionsToSpdy moves single transaction %p "
+             "into SpdySession %p\n", mTransaction.get(), mSpdySession.get()));
+        nsresult rv = AddTransaction(mTransaction, mPriority);
+        if (NS_FAILED(rv)) {
+            return rv;
+        }
+    } else {
+        int32_t count = list.Length();
+
+        LOG(("nsHttpConnection::MoveTransactionsToSpdy moving transaction list len=%d "
+             "into SpdySession %p\n", count, mSpdySession.get()));
+
+        if (!count) {
+            mTransaction->Close(NS_ERROR_ABORT);
+            return NS_ERROR_ABORT;
+        }
+
+        for (int32_t index = 0; index < count; ++index) {
+            nsresult rv = AddTransaction(list[index], mPriority);
+            if (NS_FAILED(rv)) {
+                return rv;
+            }
+        }
+    }
+
+    return NS_OK;
+}
+
+void
+nsHttpConnection::Start0RTTSpdy(uint8_t spdyVersion)
+{
+    LOG(("nsHttpConnection::Start0RTTSpdy [this=%p]", this));
+    mDid0RTTSpdy = true;
+    mUsingSpdyVersion = spdyVersion;
+    mSpdySession = ASpdySession::NewSpdySession(spdyVersion, mSocketTransport,
+                                                true);
+
+    nsTArray<RefPtr<nsAHttpTransaction> > list;
+    nsresult rv = TryTakeSubTransactions(list);
+    if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) {
+        LOG(("nsHttpConnection::Start0RTTSpdy [this=%p] failed taking "
+             "subtransactions rv=%" PRIx32 , this, static_cast<uint32_t>(rv)));
+        return;
+    }
+
+    rv = MoveTransactionsToSpdy(rv, list);
+    if (NS_FAILED(rv)) {
+        LOG(("nsHttpConnection::Start0RTTSpdy [this=%p] failed moving "
+             "transactions rv=%" PRIx32 , this, static_cast<uint32_t>(rv)));
+        return;
+    }
+
+    mTransaction = mSpdySession;
+}
+
 void
 nsHttpConnection::StartSpdy(uint8_t spdyVersion)
 {
-    LOG(("nsHttpConnection::StartSpdy [this=%p]\n", this));
+    LOG(("nsHttpConnection::StartSpdy [this=%p, mDid0RTTSpdy=%d]\n", this, mDid0RTTSpdy));
 
-    MOZ_ASSERT(!mSpdySession);
+    MOZ_ASSERT(!mSpdySession || mDid0RTTSpdy);
 
     mUsingSpdyVersion = spdyVersion;
     mEverUsedSpdy = true;
-    mSpdySession = ASpdySession::NewSpdySession(spdyVersion, mSocketTransport);
+
+    if (!mDid0RTTSpdy) {
+        mSpdySession = ASpdySession::NewSpdySession(spdyVersion, mSocketTransport,
+                                                    false);
+    }
 
     if (!mReportedSpdy) {
         mReportedSpdy = true;
         gHttpHandler->ConnMgr()->ReportSpdyConnection(this, true);
     }
 
     // Setting the connection as reused allows some transactions that fail
     // with NS_ERROR_NET_RESET to be restarted and SPDY uses that code
@@ -180,37 +278,23 @@ nsHttpConnection::StartSpdy(uint8_t spdy
     // a server goaway was generated).
     mIsReused = true;
 
     // If mTransaction is a pipeline object it might represent
     // several requests. If so, we need to unpack that and
     // pack them all into a new spdy session.
 
     nsTArray<RefPtr<nsAHttpTransaction> > list;
-    nsresult rv = mTransaction->TakeSubTransactions(list);
+    nsresult rv = NS_OK;
+    if (!mDid0RTTSpdy) {
+        rv = TryTakeSubTransactions(list);
 
-    if (rv == NS_ERROR_ALREADY_OPENED) {
-        // Has the interface for TakeSubTransactions() changed?
-        LOG(("TakeSubTransactions somehow called after "
-             "nsAHttpTransaction began processing\n"));
-        MOZ_ASSERT(false,
-                   "TakeSubTransactions somehow called after "
-                   "nsAHttpTransaction began processing");
-        mTransaction->Close(NS_ERROR_ABORT);
-        return;
-    }
-
-    if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) {
-        // Has the interface for TakeSubTransactions() changed?
-        LOG(("unexpected rv from nnsAHttpTransaction::TakeSubTransactions()"));
-        MOZ_ASSERT(false,
-                   "unexpected result from "
-                   "nsAHttpTransaction::TakeSubTransactions()");
-        mTransaction->Close(NS_ERROR_ABORT);
-        return;
+        if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) {
+            return;
+        }
     }
 
     if (NeedSpdyTunnel()) {
         LOG3(("nsHttpConnection::StartSpdy %p Connecting To a HTTP/2 "
               "Proxy and Need Connect", this));
         MOZ_ASSERT(mProxyConnectStream);
 
         mProxyConnectStream = nullptr;
@@ -222,45 +306,21 @@ nsHttpConnection::StartSpdy(uint8_t spdy
     if (spdyProxy) {
         RefPtr<nsHttpConnectionInfo> wildCardProxyCi;
         mConnInfo->CreateWildCard(getter_AddRefs(wildCardProxyCi));
         gHttpHandler->ConnMgr()->MoveToWildCardConnEntry(mConnInfo,
                                                          wildCardProxyCi, this);
         mConnInfo = wildCardProxyCi;
     }
 
-    if (NS_FAILED(rv)) { // includes NS_ERROR_NOT_IMPLEMENTED
-        MOZ_ASSERT(list.IsEmpty(), "sub transaction list not empty");
-
-        // This is ok - treat mTransaction as a single real request.
-        // Wrap the old http transaction into the new spdy session
-        // as the first stream.
-        LOG(("nsHttpConnection::StartSpdy moves single transaction %p "
-             "into SpdySession %p\n", mTransaction.get(), mSpdySession.get()));
-        rv = AddTransaction(mTransaction, mPriority);
+    if (!mDid0RTTSpdy) {
+        rv = MoveTransactionsToSpdy(rv, list);
         if (NS_FAILED(rv)) {
             return;
         }
-    } else {
-        int32_t count = list.Length();
-
-        LOG(("nsHttpConnection::StartSpdy moving transaction list len=%d "
-             "into SpdySession %p\n", count, mSpdySession.get()));
-
-        if (!count) {
-            mTransaction->Close(NS_ERROR_ABORT);
-            return;
-        }
-
-        for (int32_t index = 0; index < count; ++index) {
-            rv = AddTransaction(list[index], mPriority);
-            if (NS_FAILED(rv)) {
-                return;
-            }
-        }
     }
 
     // Disable TCP Keepalives - use SPDY ping instead.
     rv = DisableTCPKeepalives();
     if (NS_FAILED(rv)) {
         LOG(("nsHttpConnection::StartSpdy [%p] DisableTCPKeepalives failed "
              "rv[0x%" PRIx32 "]", this, static_cast<uint32_t>(rv)));
     }
@@ -316,57 +376,63 @@ nsHttpConnection::EnsureNPNComplete(nsre
     rv = ssl->GetNegotiatedNPN(negotiatedNPN);
     if (!m0RTTChecked && (rv == NS_ERROR_NOT_CONNECTED) &&
         !mConnInfo->UsingProxy()) {
         // There is no ALPN info (yet!). We need to consider doing 0RTT. We
         // will do so if there is ALPN information from a previous session
         // (AlpnEarlySelection), we are using HTTP/1, and the request data can
         // be safely retried.
         m0RTTChecked = true;
-        nsAutoCString earlyNegotiatedNPN;
-        nsresult rvEarlyAlpn = ssl->GetAlpnEarlySelection(earlyNegotiatedNPN);
+        nsresult rvEarlyAlpn = ssl->GetAlpnEarlySelection(mEarlyNegotiatedALPN);
         if (NS_FAILED(rvEarlyAlpn)) {
             // if ssl->DriveHandshake() has never been called the value
             // for AlpnEarlySelection is still not set. So call it here and
             // check again.
             LOG(("nsHttpConnection::EnsureNPNComplete %p - "
                  "early selected alpn not available, we will try one more time.",
                  this));
             // Let's do DriveHandshake again.
             rv = ssl->DriveHandshake();
             if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) {
                 goto npnComplete;
             }
 
             // Check NegotiatedNPN first.
             rv = ssl->GetNegotiatedNPN(negotiatedNPN);
             if (rv == NS_ERROR_NOT_CONNECTED) {
-                rvEarlyAlpn = ssl->GetAlpnEarlySelection(earlyNegotiatedNPN);
+                rvEarlyAlpn = ssl->GetAlpnEarlySelection(mEarlyNegotiatedALPN);
             }
         }
 
         if (NS_FAILED(rvEarlyAlpn)) {
             LOG(("nsHttpConnection::EnsureNPNComplete %p - "
                  "early selected alpn not available", this));
             mEarlyDataNegotiated = false;
         } else {
             LOG(("nsHttpConnection::EnsureNPNComplete %p -"
-                 "early selected alpn: %s", this, earlyNegotiatedNPN.get()));
+                 "early selected alpn: %s", this, mEarlyNegotiatedALPN.get()));
             uint32_t infoIndex;
             const SpdyInformation *info = gHttpHandler->SpdyInfo();
-            // We are doing 0RTT only with Http/1 right now!
-            if (NS_FAILED(info->GetNPNIndex(earlyNegotiatedNPN, &infoIndex))) {
+            if (NS_FAILED(info->GetNPNIndex(mEarlyNegotiatedALPN, &infoIndex))) {
+                // This is the HTTP/1 case.
                 // Check if early-data is allowed for this transaction.
                 if (mTransaction->Do0RTT()) {
                     LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] - We "
-                         "can do 0RTT!", this));
+                         "can do 0RTT (http/1)!", this));
                     mWaitingFor0RTTResponse = true;
                 }
-                mEarlyDataNegotiated = true;
+            } else {
+                // We have h2, we can at least 0-RTT the preamble and opening
+                // SETTINGS, etc, and maybe some of the first request
+                LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] - Starting "
+                     "0RTT for h2!", this));
+                mWaitingFor0RTTResponse = true;
+                Start0RTTSpdy(info->Version[infoIndex]);
             }
+            mEarlyDataNegotiated = true;
         }
     }
 
     if (rv == NS_ERROR_NOT_CONNECTED) {
         if (mWaitingFor0RTTResponse) {
             aOut0RTTWriteHandshakeValue = mTransaction->ReadSegments(this,
                 nsIOService::gDefaultSegmentSize, &aOut0RTTBytesWritten);
             if (NS_FAILED(aOut0RTTWriteHandshakeValue) &&
@@ -386,75 +452,97 @@ nsHttpConnection::EnsureNPNComplete(nsre
         return false;
     }
 
     if (NS_SUCCEEDED(rv)) {
         LOG(("nsHttpConnection::EnsureNPNComplete %p [%s] negotiated to '%s'%s\n",
              this, mConnInfo->HashKey().get(), negotiatedNPN.get(),
              mTLSFilter ? " [Double Tunnel]" : ""));
 
-        bool ealyDataAccepted = false;
+        bool earlyDataAccepted = false;
         if (mWaitingFor0RTTResponse) {
             // Check if early data has been accepted.
-            rv = ssl->GetEarlyDataAccepted(&ealyDataAccepted);
+            rv = ssl->GetEarlyDataAccepted(&earlyDataAccepted);
             LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] - early data "
-                 "that was sent during 0RTT %s been accepted.",
-                 this, ealyDataAccepted ? "has" : "has not"));
+                 "that was sent during 0RTT %s been accepted [rv=%" PRIx32 "].",
+                 this, earlyDataAccepted ? "has" : "has not", static_cast<uint32_t>(rv)));
 
             if (NS_FAILED(rv) ||
-                NS_FAILED(mTransaction->Finish0RTT(!ealyDataAccepted))) {
+                NS_FAILED(mTransaction->Finish0RTT(!earlyDataAccepted, negotiatedNPN != mEarlyNegotiatedALPN))) {
+                LOG(("nsHttpConection::EnsureNPNComplete [this=%p] closing transaction %p", this, mTransaction.get()));
                 mTransaction->Close(NS_ERROR_NET_RESET);
                 goto npnComplete;
             }
         }
 
         int16_t tlsVersion;
         ssl->GetSSLVersionUsed(&tlsVersion);
         // Send the 0RTT telemetry only for tls1.3
         if (tlsVersion > nsISSLSocketControl::TLS_VERSION_1_2) {
             Telemetry::Accumulate(Telemetry::TLS_EARLY_DATA_NEGOTIATED,
                 (!mEarlyDataNegotiated) ? TLS_EARLY_DATA_NOT_AVAILABLE
                     : ((mWaitingFor0RTTResponse) ? TLS_EARLY_DATA_AVAILABLE_AND_USED
                                                  : TLS_EARLY_DATA_AVAILABLE_BUT_NOT_USED));
             if (mWaitingFor0RTTResponse) {
                 Telemetry::Accumulate(Telemetry::TLS_EARLY_DATA_ACCEPTED,
-                                      ealyDataAccepted);
+                                      earlyDataAccepted);
             }
-            if (ealyDataAccepted) {
+            if (earlyDataAccepted) {
                 Telemetry::Accumulate(Telemetry::TLS_EARLY_DATA_BYTES_WRITTEN,
                                       mContentBytesWritten0RTT);
             }
         }
         mWaitingFor0RTTResponse = false;
 
-        if (!ealyDataAccepted) {
+        if (!earlyDataAccepted) {
+            LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] early data not accepted", this));
             uint32_t infoIndex;
             const SpdyInformation *info = gHttpHandler->SpdyInfo();
             if (NS_SUCCEEDED(info->GetNPNIndex(negotiatedNPN, &infoIndex))) {
                 StartSpdy(info->Version[infoIndex]);
             }
         } else {
           LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] - %" PRId64 " bytes "
                "has been sent during 0RTT.", this, mContentBytesWritten0RTT));
           mContentBytesWritten = mContentBytesWritten0RTT;
+          if (mSpdySession) {
+              // We had already started 0RTT-spdy, now we need to fully set up
+              // spdy, since we know we're sticking with it.
+              LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] - finishing "
+                   "StartSpdy for 0rtt spdy session %p", this, mSpdySession.get()));
+              StartSpdy(mSpdySession->SpdyVersion());
+          }
         }
 
         Telemetry::Accumulate(Telemetry::SPDY_NPN_CONNECT, UsingSpdy());
     }
 
 npnComplete:
-    LOG(("nsHttpConnection::EnsureNPNComplete setting complete to true"));
+    LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] setting complete to true", this));
     mNPNComplete = true;
     if (mWaitingFor0RTTResponse) {
+        // Didn't get 0RTT OK, back out of the "attempting 0RTT" state
         mWaitingFor0RTTResponse = false;
-        if (NS_FAILED(mTransaction->Finish0RTT(true))) {
+        LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] 0rtt failed", this));
+        if (NS_FAILED(mTransaction->Finish0RTT(true, negotiatedNPN != mEarlyNegotiatedALPN))) {
             mTransaction->Close(NS_ERROR_NET_RESET);
         }
         mContentBytesWritten0RTT = 0;
     }
+
+    if (mDid0RTTSpdy && negotiatedNPN != mEarlyNegotiatedALPN) {
+        // Reset the work done by Start0RTTSpdy
+        LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] resetting Start0RTTSpdy", this));
+        mUsingSpdyVersion = 0;
+        mTransaction = nullptr;
+        mSpdySession = nullptr;
+        // We have to reset this here, just in case we end up starting spdy again,
+        // so it can actually do everything it needs to do.
+        mDid0RTTSpdy = false;
+    }
     return true;
 }
 
 void
 nsHttpConnection::OnTunnelNudged(TLSFilterTransaction *trans)
 {
     MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
     LOG(("nsHttpConnection::OnTunnelNudged %p\n", this));
--- a/netwerk/protocol/http/nsHttpConnection.h
+++ b/netwerk/protocol/http/nsHttpConnection.h
@@ -243,16 +243,23 @@ private:
     // Makes certain the SSL handshake is complete and NPN negotiation
     // has had a chance to happen
     bool     EnsureNPNComplete(nsresult &aOut0RTTWriteHandshakeValue,
                                uint32_t &aOut0RTTBytesWritten);
     void     SetupSSL();
 
     // Start the Spdy transaction handler when NPN indicates spdy/*
     void     StartSpdy(uint8_t versionLevel);
+    // Like the above, but do the bare minimum to do 0RTT data, so we can back
+    // it out, if necessary
+    void     Start0RTTSpdy(uint8_t versionLevel);
+
+    // Helpers for Start*Spdy
+    nsresult TryTakeSubTransactions(nsTArray<RefPtr<nsAHttpTransaction> > &list);
+    nsresult MoveTransactionsToSpdy(nsresult status, nsTArray<RefPtr<nsAHttpTransaction> > &list);
 
     // Directly Add a transaction to an active connection for SPDY
     nsresult AddTransaction(nsAHttpTransaction *, int32_t);
 
     // Used to set TCP keepalives for fast detection of dead connections during
     // an initial period, and slower detection for long-lived connections.
     nsresult StartShortLivedTCPKeepalives();
     nsresult StartLongLivedTCPKeepalives();
@@ -365,14 +372,16 @@ private:
     bool                            mWaitingFor0RTTResponse; // We have are
                                                              // sending 0RTT
                                                              // data and we
                                                              // are waiting
                                                              // for the end of
                                                              // the handsake.
     int64_t                        mContentBytesWritten0RTT;
     bool                           mEarlyDataNegotiated; //Only used for telemetry
+    nsCString                      mEarlyNegotiatedALPN;
+    bool                           mDid0RTTSpdy;
 };
 
 } // namespace net
 } // namespace mozilla
 
 #endif // nsHttpConnection_h__
--- a/netwerk/protocol/http/nsHttpTransaction.cpp
+++ b/netwerk/protocol/http/nsHttpTransaction.cpp
@@ -2383,17 +2383,17 @@ nsHttpTransaction::Do0RTT()
    if (mRequestHead->IsSafeMethod() &&
        !mConnection->IsProxyConnectInProgress()) {
      m0RTTInProgress = true;
    }
    return m0RTTInProgress;
 }
 
 nsresult
-nsHttpTransaction::Finish0RTT(bool aRestart)
+nsHttpTransaction::Finish0RTT(bool aRestart, bool aAlpnChanged /* ignored */)
 {
     MOZ_ASSERT(m0RTTInProgress);
     m0RTTInProgress = false;
     if (aRestart) {
         // Reset request headers to be sent again.
         nsCOMPtr<nsISeekableStream> seekable =
             do_QueryInterface(mRequestStream);
         if (seekable) {
--- a/netwerk/protocol/http/nsHttpTransaction.h
+++ b/netwerk/protocol/http/nsHttpTransaction.h
@@ -163,17 +163,17 @@ public:
     mozilla::TimeStamp GetConnectEnd();
     mozilla::TimeStamp GetRequestStart();
     mozilla::TimeStamp GetResponseStart();
     mozilla::TimeStamp GetResponseEnd();
 
     int64_t GetTransferSize() { return mTransferSize; }
 
     bool Do0RTT() override;
-    nsresult Finish0RTT(bool aRestart) override;
+    nsresult Finish0RTT(bool aRestart, bool aAlpnChanged /* ignored */) override;
 private:
     friend class DeleteHttpTransaction;
     virtual ~nsHttpTransaction();
 
     nsresult Restart();
     nsresult RestartInProgress();
     char    *LocateHttpStart(char *buf, uint32_t len,
                              bool aAllowPartialMatch);