Have RST_STREAM & STOP_SENDING do expected closes for IETF QUIC (V99)

This change connects up the RST_STREAM and STOP_SENDING code so that
- when a RST_STREAM is sent, the sending node closes the stream for write only.
- when a RST_STREAM is received, the receiving node closes the stream for read only.
- when a STOP_SENDING is received, the node responds with a RST_STREAM and
  closes the stream in the write direction (from the perspective of the node
  receiving the STOP_SENDING)

Note that this implementation will send a RST_STREAM + STOP_SENDING, each causing the
stream to be closed in one direction. The two together cause the stream to be closed in
both directions, effecting a full close on the stream.

n/a, only for Version 99/IETF QUIC and controlled by the v99 flag

NO_BUG=Feature

Merge internal changes: 230929605, 233058905

R=rch@chromium.org

Change-Id: I622dff1297efc90971f025e70c574dcb6f68bbe2
Reviewed-on: https://chromium-review.googlesource.com/c/1461077
Reviewed-by: Ryan Hamilton <rch@chromium.org>
Commit-Queue: Frank Kastenholz <fkastenholz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#631814}
diff --git a/net/quic/bidirectional_stream_quic_impl_unittest.cc b/net/quic/bidirectional_stream_quic_impl_unittest.cc
index 014dfee..1dfb060 100644
--- a/net/quic/bidirectional_stream_quic_impl_unittest.cc
+++ b/net/quic/bidirectional_stream_quic_impl_unittest.cc
@@ -712,7 +712,8 @@
       QuicTestPacketMaker* maker) {
     std::unique_ptr<quic::QuicReceivedPacket> packet(
         maker->MakeRstPacket(packet_number, include_version, stream_id_,
-                             quic::QUIC_STREAM_CANCELLED, bytes_written));
+                             quic::QUIC_STREAM_CANCELLED, bytes_written,
+                             /*include_stop_sending_if_v99=*/true));
     DVLOG(2) << "packet(" << packet_number << "): " << std::endl
              << quic::QuicTextUtils::HexDump(packet->AsStringPiece());
     return packet;
diff --git a/net/quic/quic_chromium_client_session_test.cc b/net/quic/quic_chromium_client_session_test.cc
index 3a2ccfd..f12ef85 100644
--- a/net/quic/quic_chromium_client_session_test.cc
+++ b/net/quic/quic_chromium_client_session_test.cc
@@ -528,7 +528,8 @@
     quic_data.AddWrite(
         SYNCHRONOUS, client_maker_.MakeRstPacket(
                          3, true, GetNthClientInitiatedBidirectionalStreamId(0),
-                         quic::QUIC_RST_ACKNOWLEDGEMENT));
+                         quic::QUIC_STREAM_CANCELLED, 0,
+                         /*include_stop_sending_if_v99=*/false));
     // After the STREAM_ID_BLOCKED is sent, receive a MAX_STREAM_ID to increase
     // the limit.
     quic_data.AddRead(
@@ -568,6 +569,15 @@
                                GetNthClientInitiatedBidirectionalStreamId(0),
                                quic::QUIC_STREAM_CANCELLED, 0);
   session_->OnRstStream(rst);
+  if (version_ == quic::QUIC_VERSION_99) {
+    // For version99, to close the stream completely, we also must receive a
+    // STOP_SENDING frame:
+    quic::QuicStopSendingFrame stop_sending(
+        quic::kInvalidControlFrameId,
+        GetNthClientInitiatedBidirectionalStreamId(0),
+        quic::QUIC_STREAM_CANCELLED);
+    session_->OnStopSendingFrame(stop_sending);
+  }
   // Pump the message loop to read the max stream id packet.
   base::RunLoop().RunUntilIdle();
 
@@ -656,10 +666,13 @@
         SYNCHRONOUS,
         client_maker_.MakeStreamIdBlockedPacket(
             2, true, GetNthClientInitiatedBidirectionalStreamId(49)));
+    // This node receives the RST_STREAM+STOP_SENDING, it responds
+    // with only a RST_STREAM.
     quic_data.AddWrite(
         SYNCHRONOUS, client_maker_.MakeRstPacket(
                          3, true, GetNthClientInitiatedBidirectionalStreamId(0),
-                         quic::QUIC_RST_ACKNOWLEDGEMENT));
+                         quic::QUIC_STREAM_CANCELLED, 0,
+                         /*include_stop_sending_if_v99=*/false));
   } else {
     quic_data.AddWrite(
         SYNCHRONOUS, client_maker_.MakeRstPacket(
@@ -698,6 +711,15 @@
                                GetNthClientInitiatedBidirectionalStreamId(0),
                                quic::QUIC_STREAM_CANCELLED, 0);
   session_->OnRstStream(rst);
+  if (version_ == quic::QUIC_VERSION_99) {
+    // For version99, we require a STOP_SENDING as well as a RESET_STREAM to
+    // fully close the stream.
+    quic::QuicStopSendingFrame stop_sending(
+        quic::kInvalidControlFrameId,
+        GetNthClientInitiatedBidirectionalStreamId(0),
+        quic::QUIC_STREAM_CANCELLED);
+    session_->OnStopSendingFrame(stop_sending);
+  }
   EXPECT_EQ(kMaxOpenStreams - 1, session_->GetNumOpenOutgoingStreams());
 
   quic_data.Resume();
diff --git a/net/quic/quic_chromium_client_stream_test.cc b/net/quic/quic_chromium_client_stream_test.cc
index 45de6530..329fde9 100644
--- a/net/quic/quic_chromium_client_stream_test.cc
+++ b/net/quic/quic_chromium_client_stream_test.cc
@@ -373,13 +373,36 @@
       quic::kInvalidControlFrameId,
       quic::test::GetNthClientInitiatedBidirectionalStreamId(GetParam(), 0),
       quic::QUIC_STREAM_CANCELLED, 0);
-  EXPECT_CALL(
-      session_,
-      SendRstStream(
-          quic::test::GetNthClientInitiatedBidirectionalStreamId(GetParam(), 0),
-          quic::QUIC_RST_ACKNOWLEDGEMENT, 0));
-  stream_->OnStreamReset(rst);
+  if (GetParam() != quic::QUIC_VERSION_99) {
+    EXPECT_CALL(
+        session_,
+        SendRstStream(quic::test::GetNthClientInitiatedBidirectionalStreamId(
+                          GetParam(), 0),
+                      quic::QUIC_RST_ACKNOWLEDGEMENT, 0));
+  } else {
+    // Intercept & check that the call to the QuicConnection's OnStreamReast
+    // has correct stream ID and error code -- for V99/IETF Quic, it should
+    // have the STREAM_CANCELLED error code, not RST_ACK... Capture
+    // OnStreamReset (rather than SendRstStream) because the V99 path bypasses
+    // SendRstStream, calling SendRstStreamInner directly. Mocking
+    // SendRstStreamInner is problematic since the test relies on it to perform
+    // the closing operations and getting the stream in the correct state.
+    EXPECT_CALL(
+        *(static_cast<quic::test::MockQuicConnection*>(session_.connection())),
+        OnStreamReset(stream_->id(), quic::QUIC_STREAM_CANCELLED));
+  }
 
+  stream_->OnStreamReset(rst);
+  if (GetParam() == quic::QUIC_VERSION_99) {
+    // Make a STOP_SENDING frame and pass it to QUIC. For V99/IETF QUIC,
+    // we need both a REST_STREAM and a STOP_SENDING to effect a closed
+    // stream.
+    quic::QuicStopSendingFrame stop_sending_frame(
+        quic::kInvalidControlFrameId,
+        quic::test::GetNthClientInitiatedBidirectionalStreamId(GetParam(), 0),
+        quic::QUIC_STREAM_CANCELLED);
+    session_.OnStopSendingFrame(stop_sending_frame);
+  }
   EXPECT_FALSE(handle_->IsOpen());
   EXPECT_EQ(quic::QUIC_STREAM_CANCELLED, handle_->stream_error());
 }
diff --git a/net/quic/quic_network_transaction_unittest.cc b/net/quic/quic_network_transaction_unittest.cc
index f5bee85..334b38a 100644
--- a/net/quic/quic_network_transaction_unittest.cc
+++ b/net/quic/quic_network_transaction_unittest.cc
@@ -369,7 +369,8 @@
       quic::QuicRstStreamErrorCode error_code,
       size_t bytes_written) {
     return client_maker_.MakeRstPacket(num, false, stream_id, error_code,
-                                       bytes_written);
+                                       bytes_written,
+                                       /*include_stop_sending_if_v99=*/true);
   }
 
   std::unique_ptr<quic::QuicEncryptedPacket>
@@ -8480,7 +8481,8 @@
         SYNCHRONOUS,
         client_maker->MakeRstPacket(
             4, false, GetNthClientInitiatedBidirectionalStreamId(0),
-            quic::QUIC_STREAM_CANCELLED, client_data_offset));
+            quic::QUIC_STREAM_CANCELLED, client_data_offset,
+            /*include_stop_sending_if_v99=*/true));
 
     headers = client_maker->ConnectRequestHeaders("mail.example.org:443");
     headers["proxy-authorization"] = "Basic Zm9vOmJheg==";
diff --git a/net/quic/quic_proxy_client_socket_unittest.cc b/net/quic/quic_proxy_client_socket_unittest.cc
index 056d0016..ab8e4bd 100644
--- a/net/quic/quic_proxy_client_socket_unittest.cc
+++ b/net/quic/quic_proxy_client_socket_unittest.cc
@@ -280,6 +280,20 @@
         largest_received, smallest_received, least_unacked, kSendFeedback);
   }
 
+  std::unique_ptr<quic::QuicReceivedPacket> ConstructAckAndRstOnlyPacket(
+      uint64_t packet_number,
+      quic::QuicRstStreamErrorCode error_code,
+      uint64_t largest_received,
+      uint64_t smallest_received,
+      uint64_t least_unacked,
+      size_t bytes_written) {
+    return client_maker_.MakeAckAndRstPacket(
+        packet_number, !kIncludeVersion, client_data_stream_id1_, error_code,
+        largest_received, smallest_received, least_unacked, kSendFeedback,
+        bytes_written,
+        /*include_stop_sending=*/false);
+  }
+
   std::unique_ptr<quic::QuicReceivedPacket> ConstructAckAndRstPacket(
       uint64_t packet_number,
       quic::QuicRstStreamErrorCode error_code,
@@ -290,7 +304,8 @@
     return client_maker_.MakeAckAndRstPacket(
         packet_number, !kIncludeVersion, client_data_stream_id1_, error_code,
         largest_received, smallest_received, least_unacked, kSendFeedback,
-        bytes_written);
+        bytes_written,
+        /*include_stop_sending_if_v99=*/true);
   }
 
   std::unique_ptr<quic::QuicReceivedPacket> ConstructRstPacket(
@@ -299,7 +314,8 @@
       size_t bytes_written) {
     return client_maker_.MakeRstPacket(packet_number, !kIncludeVersion,
                                        client_data_stream_id1_, error_code,
-                                       bytes_written);
+                                       bytes_written,
+                                       /*include_stop_sending_if_v99=*/true);
   }
 
   std::unique_ptr<quic::QuicReceivedPacket> ConstructConnectRequestPacket(
@@ -386,7 +402,8 @@
       size_t bytes_written) {
     return server_maker_.MakeRstPacket(packet_number, !kIncludeVersion,
                                        client_data_stream_id1_, error_code,
-                                       bytes_written);
+                                       bytes_written,
+                                       /*include_stop_sending_if_v99=*/true);
   }
 
   std::unique_ptr<quic::QuicReceivedPacket> ConstructServerDataPacket(
@@ -1622,10 +1639,9 @@
     mock_quic_data_.AddWrite(
         ASYNC, ConstructAckAndMultipleDataFramesPacket(
                    3, 1, 1, 1, 0, {header, quic::QuicString(kMsg2, kLen2)}));
-    mock_quic_data_.AddWrite(
-        SYNCHRONOUS,
-        ConstructAckAndRstPacket(4, quic::QUIC_RST_ACKNOWLEDGEMENT, 2, 2, 1,
-                                 header.length() + kLen2));
+    mock_quic_data_.AddWrite(SYNCHRONOUS, ConstructAckAndRstOnlyPacket(
+                                              4, quic::QUIC_STREAM_CANCELLED, 2,
+                                              2, 1, header.length() + kLen2));
   }
 
   Initialize();
@@ -1757,10 +1773,9 @@
     mock_quic_data_.AddWrite(
         ASYNC, ConstructAckAndMultipleDataFramesPacket(
                    3, 1, 1, 1, 0, {header, quic::QuicString(kMsg1, kLen1)}));
-    mock_quic_data_.AddWrite(
-        SYNCHRONOUS,
-        ConstructAckAndRstPacket(4, quic::QUIC_RST_ACKNOWLEDGEMENT, 2, 2, 1,
-                                 header.length() + kLen1));
+    mock_quic_data_.AddWrite(SYNCHRONOUS, ConstructAckAndRstOnlyPacket(
+                                              4, quic::QUIC_STREAM_CANCELLED, 2,
+                                              2, 1, header.length() + kLen1));
   }
 
   Initialize();
diff --git a/net/quic/quic_stream_factory_test.cc b/net/quic/quic_stream_factory_test.cc
index a020a1f..63505fd 100644
--- a/net/quic/quic_stream_factory_test.cc
+++ b/net/quic/quic_stream_factory_test.cc
@@ -4622,7 +4622,8 @@
   socket_data1.AddWrite(
       SYNCHRONOUS, client_maker_.MakeRstPacket(
                        5, false, GetNthClientInitiatedBidirectionalStreamId(1),
-                       quic::QUIC_STREAM_CANCELLED, 0));
+                       quic::QUIC_STREAM_CANCELLED, 0,
+                       /*include_stop_sending_if_v99=*/true));
 
   socket_data1.AddSocketDataToFactory(socket_factory_.get());
 
@@ -4835,7 +4836,8 @@
   quic_data3.AddWrite(
       SYNCHRONOUS, client_maker_.MakeRstPacket(
                        5, false, GetNthClientInitiatedBidirectionalStreamId(0),
-                       quic::QUIC_STREAM_CANCELLED, 0));
+                       quic::QUIC_STREAM_CANCELLED, 0,
+                       /*include_stop_sending_if_v99=*/true));
   quic_data3.AddSocketDataToFactory(socket_factory_.get());
 
   // Fast forward to fire the migrate back timer and verify the session
@@ -5578,7 +5580,8 @@
   socket_data1.AddWrite(
       SYNCHRONOUS, client_maker_.MakeRstPacket(
                        4, false, GetNthClientInitiatedBidirectionalStreamId(1),
-                       quic::QUIC_STREAM_CANCELLED, 0));
+                       quic::QUIC_STREAM_CANCELLED, 0,
+                       /*include_stop_sending_if_v99=*/true));
 
   socket_data1.AddSocketDataToFactory(socket_factory_.get());
 
@@ -5704,7 +5707,8 @@
       SYNCHRONOUS,
       client_maker_.MakeRstPacket(packet_number++, true,
                                   GetNthClientInitiatedBidirectionalStreamId(1),
-                                  quic::QUIC_STREAM_CANCELLED, 0));
+                                  quic::QUIC_STREAM_CANCELLED, 0,
+                                  /*include_stop_sending_if_v99=*/true));
   socket_data1.AddRead(
       ASYNC,
       ConstructOkResponsePacket(
@@ -5845,7 +5849,8 @@
       SYNCHRONOUS,
       client_maker_.MakeRstPacket(packet_number++, true,
                                   GetNthClientInitiatedBidirectionalStreamId(1),
-                                  quic::QUIC_STREAM_CANCELLED, 0));
+                                  quic::QUIC_STREAM_CANCELLED, 0,
+                                  /*include_stop_sending_if_v99=*/true));
   socket_data1.AddWrite(
       SYNCHRONOUS,
       ConstructGetRequestPacket(packet_number++,
diff --git a/net/quic/quic_test_packet_maker.cc b/net/quic/quic_test_packet_maker.cc
index d0e5fcd..a402151 100644
--- a/net/quic/quic_test_packet_maker.cc
+++ b/net/quic/quic_test_packet_maker.cc
@@ -192,7 +192,8 @@
     bool include_version,
     quic::QuicStreamId stream_id,
     quic::QuicRstStreamErrorCode error_code) {
-  return MakeRstPacket(num, include_version, stream_id, error_code, 0);
+  return MakeRstPacket(num, include_version, stream_id, error_code, 0,
+                       /*include_stop_sending_if_v99=*/true);
 }
 
 std::unique_ptr<quic::QuicReceivedPacket> QuicTestPacketMaker::MakeRstPacket(
@@ -200,7 +201,8 @@
     bool include_version,
     quic::QuicStreamId stream_id,
     quic::QuicRstStreamErrorCode error_code,
-    size_t bytes_written) {
+    size_t bytes_written,
+    bool include_stop_sending_if_v99) {
   quic::QuicPacketHeader header;
   header.destination_connection_id = connection_id_;
   header.destination_connection_id_length = GetDestinationConnectionIdLength();
@@ -221,7 +223,7 @@
   // The STOP_SENDING frame must be outside of the if (version==99) so that it
   // stays in scope until the packet is built.
   quic::QuicStopSendingFrame stop(1, stream_id, error_code);
-  if (version_ == quic::QUIC_VERSION_99) {
+  if (include_stop_sending_if_v99 && version_ == quic::QUIC_VERSION_99) {
     frames.push_back(quic::QuicFrame(&stop));
     DVLOG(1) << "Adding frame: " << frames.back();
   }
@@ -327,7 +329,8 @@
     bool send_feedback) {
   return MakeAckAndRstPacket(num, include_version, stream_id, error_code,
                              largest_received, smallest_received, least_unacked,
-                             send_feedback, 0);
+                             send_feedback, 0,
+                             /*include_stop_sending_if_v99=*/true);
 }
 
 std::unique_ptr<quic::QuicReceivedPacket>
@@ -340,7 +343,8 @@
     uint64_t smallest_received,
     uint64_t least_unacked,
     bool send_feedback,
-    size_t bytes_written) {
+    size_t bytes_written,
+    bool include_stop_sending_if_v99) {
   quic::QuicPacketHeader header;
   header.destination_connection_id = connection_id_;
   header.destination_connection_id_length = GetDestinationConnectionIdLength();
@@ -380,7 +384,7 @@
   // The STOP_SENDING frame must be outside of the if (version==99) so that it
   // stays in scope until the packet is built.
   quic::QuicStopSendingFrame stop(1, stream_id, error_code);
-  if (version_ == quic::QUIC_VERSION_99) {
+  if (version_ == quic::QUIC_VERSION_99 && include_stop_sending_if_v99) {
     frames.push_back(quic::QuicFrame(&stop));
     DVLOG(1) << "Adding frame: " << frames.back();
   }
diff --git a/net/quic/quic_test_packet_maker.h b/net/quic/quic_test_packet_maker.h
index 0504966..de0f4a7 100644
--- a/net/quic/quic_test_packet_maker.h
+++ b/net/quic/quic_test_packet_maker.h
@@ -84,7 +84,8 @@
       bool include_version,
       quic::QuicStreamId stream_id,
       quic::QuicRstStreamErrorCode error_code,
-      size_t bytes_written);
+      size_t bytes_written,
+      bool include_stop_sending_if_v99);
 
   std::unique_ptr<quic::QuicReceivedPacket> MakeRstAndRequestHeadersPacket(
       uint64_t num,
@@ -117,7 +118,8 @@
       uint64_t smallest_received,
       uint64_t least_unacked,
       bool send_feedback,
-      size_t bytes_written);
+      size_t bytes_written,
+      bool include_stop_sending_if_v99);
   std::unique_ptr<quic::QuicReceivedPacket> MakeRstAckAndConnectionClosePacket(
       uint64_t num,
       bool include_version,
diff --git a/net/third_party/quic/core/http/end_to_end_test.cc b/net/third_party/quic/core/http/end_to_end_test.cc
index 8dcc7ef..ff4a4c2 100644
--- a/net/third_party/quic/core/http/end_to_end_test.cc
+++ b/net/third_party/quic/core/http/end_to_end_test.cc
@@ -3733,6 +3733,32 @@
   EXPECT_EQ(kStopSendingTestCode, client_stream->last_stop_sending_code());
 }
 
+TEST_P(EndToEndTest, SimpleStopSendingRstStreamTest) {
+  ASSERT_TRUE(Initialize());
+
+  // Send a request without a fin, to keep the stream open
+  SpdyHeaderBlock headers;
+  headers[":method"] = "POST";
+  headers[":path"] = "/foo";
+  headers[":scheme"] = "https";
+  headers[":authority"] = server_hostname_;
+  client_->SendMessage(headers, "", /*fin=*/false);
+  // Stream should be open
+  ASSERT_NE(nullptr, client_->latest_created_stream());
+  EXPECT_FALSE(
+      QuicStreamPeer::write_side_closed(client_->latest_created_stream()));
+  EXPECT_FALSE(
+      QuicStreamPeer::read_side_closed(client_->latest_created_stream()));
+
+  // Send a RST_STREAM+STOP_SENDING on the stream
+  // Code is not important.
+  client_->latest_created_stream()->Reset(QUIC_BAD_APPLICATION_PAYLOAD);
+  client_->WaitForResponse();
+
+  // Stream should be gone.
+  ASSERT_EQ(nullptr, client_->latest_created_stream());
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/net/third_party/quic/core/http/quic_server_session_base_test.cc b/net/third_party/quic/core/http/quic_server_session_base_test.cc
index 4ffdc75..b10db22 100644
--- a/net/third_party/quic/core/http/quic_server_session_base_test.cc
+++ b/net/third_party/quic/core/http/quic_server_session_base_test.cc
@@ -170,6 +170,29 @@
     return connection_->transport_version();
   }
 
+  // Create and inject a STOP_SENDING frame. In GOOGLE QUIC, receiving a
+  // RST_STREAM frame causes a two-way close. For IETF QUIC, RST_STREAM causes a
+  // one-way close. This method can be used to inject a STOP_SENDING, which
+  // would cause a close in the opposite direction. This allows tests to do the
+  // extra work to get a two-way (full) close where desired. Also sets up
+  // expects needed to ensure that the STOP_SENDING worked as expected.
+  void InjectStopSendingFrame(QuicStreamId stream_id,
+                              QuicRstStreamErrorCode rst_stream_code) {
+    if (transport_version() != QUIC_VERSION_99) {
+      // Only needed for version 99/IETF QUIC. Noop otherwise.
+      return;
+    }
+    QuicStopSendingFrame stop_sending(
+        kInvalidControlFrameId, stream_id,
+        static_cast<QuicApplicationErrorCode>(rst_stream_code));
+    EXPECT_CALL(owner_, OnStopSendingReceived(_)).Times(1);
+    // Expect the RESET_STREAM that is generated in response to receiving a
+    // STOP_SENDING.
+    EXPECT_CALL(*connection_, SendControlFrame(_));
+    EXPECT_CALL(*connection_, OnStreamReset(stream_id, rst_stream_code));
+    session_->OnStopSendingFrame(stop_sending);
+  }
+
   StrictMock<MockQuicSessionVisitor> owner_;
   StrictMock<MockQuicCryptoServerStreamHelper> stream_helper_;
   MockQuicConnectionHelper helper_;
@@ -216,11 +239,21 @@
                           GetNthClientInitiatedBidirectionalId(0),
                           QUIC_ERROR_PROCESSING_STREAM, 0);
   EXPECT_CALL(owner_, OnRstStreamReceived(_)).Times(1);
-  EXPECT_CALL(*connection_, SendControlFrame(_));
-  EXPECT_CALL(*connection_,
-              OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
-                            QUIC_RST_ACKNOWLEDGEMENT));
+  if (transport_version() != QUIC_VERSION_99) {
+    // For non-version 99, the RESET_STREAM will do the full close.
+    // Set up expects accordingly.
+    EXPECT_CALL(*connection_, SendControlFrame(_));
+    EXPECT_CALL(*connection_,
+                OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
+                              QUIC_RST_ACKNOWLEDGEMENT));
+  }
   visitor_->OnRstStream(rst1);
+
+  // For version-99 will create and receive a stop-sending, completing
+  // the full-close expected by this test.
+  InjectStopSendingFrame(GetNthClientInitiatedBidirectionalId(0),
+                         QUIC_ERROR_PROCESSING_STREAM);
+
   EXPECT_EQ(0u, session_->GetNumOpenIncomingStreams());
 
   // Send the same two bytes of payload in a new packet.
@@ -237,11 +270,21 @@
                           GetNthClientInitiatedBidirectionalId(0),
                           QUIC_ERROR_PROCESSING_STREAM, 0);
   EXPECT_CALL(owner_, OnRstStreamReceived(_)).Times(1);
-  EXPECT_CALL(*connection_, SendControlFrame(_));
-  EXPECT_CALL(*connection_,
-              OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
-                            QUIC_RST_ACKNOWLEDGEMENT));
+  if (transport_version() != QUIC_VERSION_99) {
+    // For non-version 99, the RESET_STREAM will do the full close.
+    // Set up expects accordingly.
+    EXPECT_CALL(*connection_, SendControlFrame(_));
+    EXPECT_CALL(*connection_,
+                OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
+                              QUIC_RST_ACKNOWLEDGEMENT));
+  }
   visitor_->OnRstStream(rst1);
+
+  // For version-99 will create and receive a stop-sending, completing
+  // the full-close expected by this test.
+  InjectStopSendingFrame(GetNthClientInitiatedBidirectionalId(0),
+                         QUIC_ERROR_PROCESSING_STREAM);
+
   EXPECT_EQ(0u, session_->GetNumOpenIncomingStreams());
 
   // Send two bytes of payload.
@@ -269,12 +312,21 @@
                          GetNthClientInitiatedBidirectionalId(0),
                          QUIC_ERROR_PROCESSING_STREAM, 0);
   EXPECT_CALL(owner_, OnRstStreamReceived(_)).Times(1);
-  EXPECT_CALL(*connection_, SendControlFrame(_));
-  EXPECT_CALL(*connection_,
-              OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
-                            QUIC_RST_ACKNOWLEDGEMENT));
+  if (transport_version() != QUIC_VERSION_99) {
+    // For non-version 99, the RESET_STREAM will do the full close.
+    // Set up expects accordingly.
+    EXPECT_CALL(*connection_, SendControlFrame(_));
+    EXPECT_CALL(*connection_,
+                OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
+                              QUIC_RST_ACKNOWLEDGEMENT));
+  }
   visitor_->OnRstStream(rst);
 
+  // For version-99 will create and receive a stop-sending, completing
+  // the full-close expected by this test.
+  InjectStopSendingFrame(GetNthClientInitiatedBidirectionalId(0),
+                         QUIC_ERROR_PROCESSING_STREAM);
+
   // If we were tracking, we'd probably want to reject this because it's data
   // past the reset point of stream 3.  As it's a closed stream we just drop the
   // data on the floor, but accept the packet because it has data for stream 5.
diff --git a/net/third_party/quic/core/http/quic_spdy_server_stream_base_test.cc b/net/third_party/quic/core/http/quic_spdy_server_stream_base_test.cc
index 056c071c..83d2508 100644
--- a/net/third_party/quic/core/http/quic_spdy_server_stream_base_test.cc
+++ b/net/third_party/quic/core/http/quic_spdy_server_stream_base_test.cc
@@ -58,10 +58,32 @@
   EXPECT_FALSE(stream_->reading_stopped());
 
   EXPECT_CALL(session_, SendRstStream(_, QUIC_STREAM_NO_ERROR, _)).Times(0);
-  EXPECT_CALL(session_, SendRstStream(_, QUIC_RST_ACKNOWLEDGEMENT, _)).Times(1);
+
+  if (session_.connection()->transport_version() != QUIC_VERSION_99) {
+    EXPECT_CALL(session_, SendRstStream(_, QUIC_RST_ACKNOWLEDGEMENT, _))
+        .Times(1);
+  } else {
+    // Intercept & check that the call to the QuicConnection's OnStreamReast
+    // has correct stream ID and error code -- for V99/IETF Quic, it should
+    // have the STREAM_CANCELLED error code, not RST_ACK... Capture
+    // OnStreamReset (rather than SendRstStream) because the V99 path bypasses
+    // SendRstStream, calling SendRstStreamInner directly. Mocking
+    // SendRstStreamInner is problematic since the test relies on it to perform
+    // the closing operations and getting the stream in the correct state.
+    EXPECT_CALL(*(static_cast<MockQuicConnection*>(session_.connection())),
+                OnStreamReset(stream_->id(), QUIC_STREAM_CANCELLED));
+  }
   QuicRstStreamFrame rst_frame(kInvalidControlFrameId, stream_->id(),
                                QUIC_STREAM_CANCELLED, 1234);
   stream_->OnStreamReset(rst_frame);
+  if (session_.connection()->transport_version() == QUIC_VERSION_99) {
+    // Create and inject a STOP SENDING frame to complete the close
+    // of the stream. This is only needed for version 99/IETF QUIC.
+    QuicStopSendingFrame stop_sending(
+        kInvalidControlFrameId, stream_->id(),
+        static_cast<QuicApplicationErrorCode>(QUIC_STREAM_CANCELLED));
+    session_.OnStopSendingFrame(stop_sending);
+  }
 
   EXPECT_TRUE(stream_->reading_stopped());
   EXPECT_TRUE(stream_->write_side_closed());
diff --git a/net/third_party/quic/core/http/quic_spdy_session_test.cc b/net/third_party/quic/core/http/quic_spdy_session_test.cc
index 82953c3..2781419 100644
--- a/net/third_party/quic/core/http/quic_spdy_session_test.cc
+++ b/net/third_party/quic/core/http/quic_spdy_session_test.cc
@@ -995,12 +995,33 @@
   EXPECT_EQ(1u, session_.GetNumOpenIncomingStreams());
 
   EXPECT_CALL(*connection_, SendControlFrame(_));
-  EXPECT_CALL(*connection_,
-              OnStreamReset(GetNthClientInitiatedBidirectionalId(0), _));
+  if (!IsVersion99()) {
+    // For version99, OnStreamReset gets called because of the STOP_SENDING,
+    // below. EXPECT the call there.
+    EXPECT_CALL(*connection_,
+                OnStreamReset(GetNthClientInitiatedBidirectionalId(0), _));
+  }
   QuicRstStreamFrame rst1(kInvalidControlFrameId,
                           GetNthClientInitiatedBidirectionalId(0),
                           QUIC_ERROR_PROCESSING_STREAM, 0);
   session_.OnRstStream(rst1);
+
+  // Create and inject a STOP_SENDING frame. In GOOGLE QUIC, receiving a
+  // RST_STREAM frame causes a two-way close. For IETF QUIC, RST_STREAM causes a
+  // one-way close.
+  if (transport_version() == QUIC_VERSION_99) {
+    // Only needed for version 99/IETF QUIC.
+    QuicStopSendingFrame stop_sending(
+        kInvalidControlFrameId, GetNthClientInitiatedBidirectionalId(0),
+        static_cast<QuicApplicationErrorCode>(QUIC_ERROR_PROCESSING_STREAM));
+    // Expect the RESET_STREAM that is generated in response to receiving a
+    // STOP_SENDING.
+    EXPECT_CALL(*connection_,
+                OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
+                              QUIC_ERROR_PROCESSING_STREAM));
+    session_.OnStopSendingFrame(stop_sending);
+  }
+
   EXPECT_EQ(0u, session_.GetNumOpenIncomingStreams());
   // Connection should remain alive.
   EXPECT_TRUE(connection_->connected());
@@ -1213,20 +1234,32 @@
   const QuicStreamOffset kByteOffset =
       1 + kInitialSessionFlowControlWindowForTest / 2;
 
+  EXPECT_CALL(*connection_, SendControlFrame(_))
+      .Times(2)
+      .WillRepeatedly(Invoke(&session_, &TestSession::ClearControlFrame));
   if (!IsVersion99()) {
-    EXPECT_CALL(*connection_, SendControlFrame(_))
-        .Times(2)
-        .WillRepeatedly(Invoke(&session_, &TestSession::ClearControlFrame));
-  } else {
-    // V99 has an additional, STOP_SENDING, frame.
-    EXPECT_CALL(*connection_, SendControlFrame(_))
-        .Times(3)
-        .WillRepeatedly(Invoke(&session_, &TestSession::ClearControlFrame));
+    // For version99 the call to OnStreamReset happens as a result of receiving
+    // the STOP SENDING, so set up the EXPECT there.
+    EXPECT_CALL(*connection_, OnStreamReset(stream->id(), _));
   }
-  EXPECT_CALL(*connection_, OnStreamReset(stream->id(), _));
   QuicRstStreamFrame rst_frame(kInvalidControlFrameId, stream->id(),
                                QUIC_STREAM_CANCELLED, kByteOffset);
   session_.OnRstStream(rst_frame);
+  // Create and inject a STOP_SENDING frame. In GOOGLE QUIC, receiving a
+  // RST_STREAM frame causes a two-way close. For IETF QUIC, RST_STREAM causes a
+  // one-way close.
+  if (transport_version() == QUIC_VERSION_99) {
+    // Only needed for version 99/IETF QUIC.
+    QuicStopSendingFrame stop_sending(
+        kInvalidControlFrameId, stream->id(),
+        static_cast<QuicApplicationErrorCode>(QUIC_STREAM_CANCELLED));
+    // Expect the RESET_STREAM that is generated in response to receiving a
+    // STOP_SENDING.
+    EXPECT_CALL(*connection_,
+                OnStreamReset(stream->id(), QUIC_STREAM_CANCELLED));
+    session_.OnStopSendingFrame(stop_sending);
+  }
+
   EXPECT_EQ(kByteOffset, session_.flow_controller()->bytes_consumed());
 }
 
diff --git a/net/third_party/quic/core/quic_connection.cc b/net/third_party/quic/core/quic_connection.cc
index 8d2d634..82a02c3 100644
--- a/net/third_party/quic/core/quic_connection.cc
+++ b/net/third_party/quic/core/quic_connection.cc
@@ -1192,7 +1192,22 @@
 }
 
 bool QuicConnection::OnStopSendingFrame(const QuicStopSendingFrame& frame) {
-  return visitor_->OnStopSendingFrame(frame);
+  DCHECK(connected_);
+
+  // Since a reset stream frame was received, this is not a connectivity probe.
+  // A probe only contains a PING and full padding.
+  UpdatePacketContent(NOT_PADDED_PING);
+
+  if (debug_visitor_ != nullptr) {
+    debug_visitor_->OnStopSendingFrame(frame);
+  }
+
+  QUIC_DLOG(INFO) << ENDPOINT << "STOP_SENDING frame received for stream: "
+                  << frame.stream_id
+                  << " with error: " << frame.application_error_code;
+
+  visitor_->OnStopSendingFrame(frame);
+  return connected_;
 }
 
 bool QuicConnection::OnPathChallengeFrame(const QuicPathChallengeFrame& frame) {
@@ -1696,6 +1711,8 @@
       ++packet_iterator;
       continue;
     }
+    // NOTE THAT RemoveFramesForStream removes only STREAM frames
+    // for the specified stream.
     RemoveFramesForStream(retransmittable_frames, id);
     if (!retransmittable_frames->empty()) {
       ++packet_iterator;
diff --git a/net/third_party/quic/core/quic_connection.h b/net/third_party/quic/core/quic_connection.h
index bf57bf0..aa86c90 100644
--- a/net/third_party/quic/core/quic_connection.h
+++ b/net/third_party/quic/core/quic_connection.h
@@ -294,6 +294,9 @@
   // Called when RTT may have changed, including when an RTT is read from
   // the config.
   virtual void OnRttChanged(QuicTime::Delta rtt) const {}
+
+  // Called when a StopSendingFrame has been parsed.
+  virtual void OnStopSendingFrame(const QuicStopSendingFrame& frame) {}
 };
 
 class QUIC_EXPORT_PRIVATE QuicConnectionHelperInterface {
diff --git a/net/third_party/quic/core/quic_dispatcher.cc b/net/third_party/quic/core/quic_dispatcher.cc
index 0f77a7a..2fdefc3 100644
--- a/net/third_party/quic/core/quic_dispatcher.cc
+++ b/net/third_party/quic/core/quic_dispatcher.cc
@@ -731,6 +731,8 @@
 
 void QuicDispatcher::OnRstStreamReceived(const QuicRstStreamFrame& frame) {}
 
+void QuicDispatcher::OnStopSendingReceived(const QuicStopSendingFrame& frame) {}
+
 void QuicDispatcher::OnConnectionAddedToTimeWaitList(
     QuicConnectionId connection_id) {
   QUIC_DLOG(INFO) << "Connection " << connection_id
diff --git a/net/third_party/quic/core/quic_dispatcher.h b/net/third_party/quic/core/quic_dispatcher.h
index 885a772..43165c4 100644
--- a/net/third_party/quic/core/quic_dispatcher.h
+++ b/net/third_party/quic/core/quic_dispatcher.h
@@ -91,6 +91,11 @@
   // Collects reset error code received on streams.
   void OnRstStreamReceived(const QuicRstStreamFrame& frame) override;
 
+  // QuicSession::Visitor interface implementation (via inheritance of
+  // QuicTimeWaitListManager::Visitor):
+  // Collects reset error code received on streams.
+  void OnStopSendingReceived(const QuicStopSendingFrame& frame) override;
+
   // QuicTimeWaitListManager::Visitor interface implementation
   // Called whenever the time wait list manager adds a new connection to the
   // time-wait list.
diff --git a/net/third_party/quic/core/quic_session.cc b/net/third_party/quic/core/quic_session.cc
index cf9eee8..c32d7be 100644
--- a/net/third_party/quic/core/quic_session.cc
+++ b/net/third_party/quic/core/quic_session.cc
@@ -185,6 +185,10 @@
     return false;
   }
 
+  if (visitor_) {
+    visitor_->OnStopSendingReceived(frame);
+  }
+
   // If stream is closed, ignore the frame
   if (IsClosedStream(stream_id)) {
     QUIC_DVLOG(1)
@@ -219,9 +223,14 @@
     return true;
   }
   stream->OnStopSending(frame.application_error_code);
-  // TODO(fkastenholz): Add in code to start rst-stream in the opposite
-  // direction once we add IETF-QUIC semantics for rst-stream.
 
+  stream->set_stream_error(
+      static_cast<QuicRstStreamErrorCode>(frame.application_error_code));
+  SendRstStreamInner(
+      stream->id(),
+      static_cast<quic::QuicRstStreamErrorCode>(frame.application_error_code),
+      stream->stream_bytes_written(),
+      /*close_write_side_only=*/true);
   return true;
 }
 
@@ -557,26 +566,34 @@
 void QuicSession::SendRstStream(QuicStreamId id,
                                 QuicRstStreamErrorCode error,
                                 QuicStreamOffset bytes_written) {
-  if (QuicContainsKey(static_stream_map_, id)) {
-    QUIC_BUG << "Cannot send RST for a static stream with ID " << id;
-    return;
-  }
+  SendRstStreamInner(id, error, bytes_written, /*close_write_side_only=*/false);
+}
 
+void QuicSession::SendRstStreamInner(QuicStreamId id,
+                                     QuicRstStreamErrorCode error,
+                                     QuicStreamOffset bytes_written,
+                                     bool close_write_side_only) {
   if (connection()->connected()) {
-    // Only send a RST_STREAM frame if still connected.
-    // Send a RST_STREAM frame. If version 99, will include
-    // an IETF-QUIC STOP_SENDING frame in the packet so that
-    // the peer also shuts down and sends a RST_STREAM back.
-    QuicConnection::ScopedPacketFlusher* flusher =
-        (connection_->transport_version() == QUIC_VERSION_99)
-            ? new QuicConnection::ScopedPacketFlusher(
-                  connection(), QuicConnection::SEND_ACK_IF_QUEUED)
-            : nullptr;
-    control_frame_manager_.WriteOrBufferRstStreamStopSending(id, error,
-                                                             bytes_written);
-    if (flusher) {
-      delete flusher;
-      flusher = nullptr;
+    // Only send if still connected.
+    if (close_write_side_only) {
+      DCHECK_EQ(QUIC_VERSION_99, connection_->transport_version());
+      // Send a RST_STREAM frame.
+      control_frame_manager_.WriteOrBufferRstStream(id, error, bytes_written);
+    } else {
+      // Send a RST_STREAM frame plus, if version 99, an IETF
+      // QUIC STOP_SENDING frame. Both sre sent to emulate
+      // the two-way close that Google QUIC's RST_STREAM does.
+      QuicConnection::ScopedPacketFlusher* flusher =
+          (connection_->transport_version() == QUIC_VERSION_99)
+              ? new QuicConnection::ScopedPacketFlusher(
+                    connection(), QuicConnection::SEND_ACK_IF_QUEUED)
+              : nullptr;
+      control_frame_manager_.WriteOrBufferRstStreamStopSending(id, error,
+                                                               bytes_written);
+      if (flusher) {
+        delete flusher;
+        flusher = nullptr;
+      }
     }
     connection_->OnStreamReset(id, error);
   }
@@ -584,7 +601,21 @@
     OnStreamDoneWaitingForAcks(id);
     return;
   }
-  CloseStreamInner(id, true);
+
+  if (!close_write_side_only) {
+    CloseStreamInner(id, true);
+    return;
+  }
+  DCHECK_EQ(QUIC_VERSION_99, connection_->transport_version());
+
+  DynamicStreamMap::iterator it = dynamic_stream_map_.find(id);
+  if (it != dynamic_stream_map_.end()) {
+    QuicStream* stream = it->second.get();
+    if (stream) {
+      stream->set_rst_sent(true);
+      stream->CloseWriteSide();
+    }
+  }
 }
 
 void QuicSession::SendGoAway(QuicErrorCode error_code,
@@ -631,7 +662,6 @@
 
 void QuicSession::CloseStreamInner(QuicStreamId stream_id, bool locally_reset) {
   QUIC_DVLOG(1) << ENDPOINT << "Closing stream " << stream_id;
-
   DynamicStreamMap::iterator it = dynamic_stream_map_.find(stream_id);
   if (it == dynamic_stream_map_.end()) {
     // When CloseStreamInner has been called recursively (via
@@ -667,7 +697,6 @@
     InsertLocallyClosedStreamsHighestOffset(
         stream_id, stream->flow_controller()->highest_received_byte_offset());
   }
-
   dynamic_stream_map_.erase(it);
   if (IsIncomingStream(stream_id)) {
     --num_dynamic_incoming_streams_;
diff --git a/net/third_party/quic/core/quic_session.h b/net/third_party/quic/core/quic_session.h
index 7044e913..009b864 100644
--- a/net/third_party/quic/core/quic_session.h
+++ b/net/third_party/quic/core/quic_session.h
@@ -62,6 +62,10 @@
 
     // Called when the session receives reset on a stream from the peer.
     virtual void OnRstStreamReceived(const QuicRstStreamFrame& frame) = 0;
+
+    // Called when the session receives a STOP_SENDING for a stream from the
+    // peer.
+    virtual void OnStopSendingReceived(const QuicStopSendingFrame& frame) = 0;
   };
 
   // CryptoHandshakeEvent enumerates the events generated by a QuicCryptoStream.
@@ -383,6 +387,17 @@
     return num_locally_closed_incoming_streams_highest_offset_;
   }
 
+  // Does actual work of sending reset-stream or reset-stream&stop-sending
+  // If the connection is not version 99/IETF QUIC, will always send a
+  // RESET_STREAM and close_write_side_only is ignored. If the connection is
+  // IETF QUIC/Version 99 then will send a RESET_STREAM and STOP_SENDING if
+  // close_write_side_only is false, just a RESET_STREAM if
+  // close_write_side_only is true.
+  virtual void SendRstStreamInner(QuicStreamId id,
+                                  QuicRstStreamErrorCode error,
+                                  QuicStreamOffset bytes_written,
+                                  bool close_write_side_only);
+
  protected:
   using StaticStreamMap = QuicSmallMap<QuicStreamId, QuicStream*, 2>;
 
diff --git a/net/third_party/quic/core/quic_session_test.cc b/net/third_party/quic/core/quic_session_test.cc
index 6f7d803..1c36112b 100644
--- a/net/third_party/quic/core/quic_session_test.cc
+++ b/net/third_party/quic/core/quic_session_test.cc
@@ -1288,19 +1288,23 @@
   const QuicStreamOffset kByteOffset =
       1 + kInitialSessionFlowControlWindowForTest / 2;
 
-  if (transport_version() == QUIC_VERSION_99) {
-    EXPECT_CALL(*connection_, SendControlFrame(_))
-        .Times(3)
-        .WillRepeatedly(Invoke(&session_, &TestSession::ClearControlFrame));
-  } else {
-    EXPECT_CALL(*connection_, SendControlFrame(_))
-        .Times(2)
-        .WillRepeatedly(Invoke(&session_, &TestSession::ClearControlFrame));
-  }
+  EXPECT_CALL(*connection_, SendControlFrame(_))
+      .Times(2)
+      .WillRepeatedly(Invoke(&session_, &TestSession::ClearControlFrame));
   EXPECT_CALL(*connection_, OnStreamReset(stream->id(), _));
+
   QuicRstStreamFrame rst_frame(kInvalidControlFrameId, stream->id(),
                                QUIC_STREAM_CANCELLED, kByteOffset);
   session_.OnRstStream(rst_frame);
+  if (transport_version() == QUIC_VERSION_99) {
+    // The test is predicated on the stream being fully closed. For V99, the
+    // RST_STREAM only does one side (the read side from the perspective of the
+    // node receiving the RST_STREAM). This is needed to fully close the
+    // stream and therefore fulfill all of the expects.
+    QuicStopSendingFrame frame(kInvalidControlFrameId, stream->id(),
+                               QUIC_STREAM_CANCELLED);
+    EXPECT_TRUE(session_.OnStopSendingFrame(frame));
+  }
   EXPECT_EQ(kByteOffset, session_.flow_controller()->bytes_consumed());
 }
 
@@ -1727,19 +1731,27 @@
 
   QuicRstStreamFrame rst_frame(kInvalidControlFrameId, stream2->id(),
                                QUIC_STREAM_CANCELLED, 1234);
+  // Just for the RST_STREAM
+  EXPECT_CALL(*connection_, SendControlFrame(_))
+      .WillOnce(Invoke(&session_, &TestSession::ClearControlFrame));
   if (transport_version() == QUIC_VERSION_99) {
-    // Once for the RST_STREAM, once for the STOP_SENDING
-    EXPECT_CALL(*connection_, SendControlFrame(_))
-        .Times(2)
-        .WillRepeatedly(Invoke(&session_, &TestSession::ClearControlFrame));
+    EXPECT_CALL(*connection_,
+                OnStreamReset(stream2->id(), QUIC_STREAM_CANCELLED));
   } else {
-    // Just for the RST_STREAM
-    EXPECT_CALL(*connection_, SendControlFrame(_))
-        .WillOnce(Invoke(&session_, &TestSession::ClearControlFrame));
+    EXPECT_CALL(*connection_,
+                OnStreamReset(stream2->id(), QUIC_RST_ACKNOWLEDGEMENT));
   }
-  EXPECT_CALL(*connection_,
-              OnStreamReset(stream2->id(), QUIC_RST_ACKNOWLEDGEMENT));
   stream2->OnStreamReset(rst_frame);
+
+  if (transport_version() == QUIC_VERSION_99) {
+    // The test is predicated on the stream being fully closed. For V99, the
+    // RST_STREAM only does one side (the read side from the perspective of the
+    // node receiving the RST_STREAM). This is needed to fully close the
+    // stream and therefore fulfill all of the expects.
+    QuicStopSendingFrame frame(kInvalidControlFrameId, stream2->id(),
+                               QUIC_STREAM_CANCELLED);
+    EXPECT_TRUE(session_.OnStopSendingFrame(frame));
+  }
   EXPECT_FALSE(QuicContainsKey(session_.zombie_streams(), stream2->id()));
   ASSERT_EQ(1u, session_.closed_streams()->size());
   EXPECT_EQ(stream2->id(), session_.closed_streams()->front()->id());
@@ -1757,6 +1769,10 @@
   EXPECT_CALL(*connection_,
               OnStreamReset(stream4->id(), QUIC_STREAM_CANCELLED));
   stream4->WriteOrBufferData(body, false, nullptr);
+  // Note well: Reset() actually closes the stream in both directions. For
+  // GOOGLE QUIC it sends a RST_STREAM (which does a 2-way close), for IETF
+  // QUIC/V99 it sends both a RST_STREAM and a STOP_SENDING (each of which
+  // closes in only one direction).
   stream4->Reset(QUIC_STREAM_CANCELLED);
   EXPECT_FALSE(QuicContainsKey(session_.zombie_streams(), stream4->id()));
   EXPECT_EQ(2u, session_.closed_streams()->size());
@@ -2286,10 +2302,24 @@
   }
 
   TestStream* stream = session_.CreateOutgoingBidirectionalStream();
+
+  // Ensure that the stream starts out open in both directions.
+  EXPECT_FALSE(QuicStreamPeer::write_side_closed(stream));
+  EXPECT_FALSE(QuicStreamPeer::read_side_closed(stream));
+
   QuicStreamId stream_id = stream->id();
   QuicStopSendingFrame frame(1, stream_id, 123);
   EXPECT_CALL(*stream, OnStopSending(123));
+  // Expect a reset to come back out.
+  EXPECT_CALL(*connection_, SendControlFrame(_));
+  EXPECT_CALL(
+      *connection_,
+      OnStreamReset(stream_id, static_cast<QuicRstStreamErrorCode>(123)));
   EXPECT_TRUE(session_.OnStopSendingFrame(frame));
+  // When the STOP_SENDING is received, the node generates a RST_STREAM,
+  // which closes the stream in the write direction. Ensure this.
+  EXPECT_FALSE(QuicStreamPeer::read_side_closed(stream));
+  EXPECT_TRUE(QuicStreamPeer::write_side_closed(stream));
 }
 
 }  // namespace
diff --git a/net/third_party/quic/core/quic_stream.cc b/net/third_party/quic/core/quic_stream.cc
index f72b4d8..7fa1def 100644
--- a/net/third_party/quic/core/quic_stream.cc
+++ b/net/third_party/quic/core/quic_stream.cc
@@ -367,7 +367,11 @@
   }
 
   stream_error_ = frame.error_code;
-  CloseWriteSide();
+  // Google QUIC closes both sides of the stream in response to a
+  // RESET_STREAM, IETF QUIC closes only the read side.
+  if (transport_version() != QUIC_VERSION_99) {
+    CloseWriteSide();
+  }
   CloseReadSide();
 }
 
diff --git a/net/third_party/quic/core/quic_stream.h b/net/third_party/quic/core/quic_stream.h
index 5dee6de..77995fb 100644
--- a/net/third_party/quic/core/quic_stream.h
+++ b/net/third_party/quic/core/quic_stream.h
@@ -345,6 +345,11 @@
   // QuicStream class.
   virtual void OnStopSending(uint16_t code);
 
+  // Close the write side of the socket.  Further writes will fail.
+  // Can be called by the subclass or internally.
+  // Does not send a FIN.  May cause the stream to be closed.
+  virtual void CloseWriteSide();
+
  protected:
   // Sends as many bytes in the first |count| buffers of |iov| to the connection
   // as the connection will consume. If FIN is consumed, the write side is
@@ -362,11 +367,6 @@
                                            QuicStreamOffset offset,
                                            bool fin);
 
-  // Close the write side of the socket.  Further writes will fail.
-  // Can be called by the subclass or internally.
-  // Does not send a FIN.  May cause the stream to be closed.
-  virtual void CloseWriteSide();
-
   // Close the read side of the socket.  May cause the stream to be closed.
   // Subclasses and consumers should use StopReading to terminate reading early
   // if expecting a FIN. Can be used directly by subclasses if not expecting a
diff --git a/net/third_party/quic/core/quic_stream_test.cc b/net/third_party/quic/core/quic_stream_test.cc
index 8e27ea3..e7feca3 100644
--- a/net/third_party/quic/core/quic_stream_test.cc
+++ b/net/third_party/quic/core/quic_stream_test.cc
@@ -127,6 +127,18 @@
     return true;
   }
 
+  bool ClearResetStreamFrame(const QuicFrame& frame) {
+    EXPECT_EQ(RST_STREAM_FRAME, frame.type);
+    DeleteFrame(&const_cast<QuicFrame&>(frame));
+    return true;
+  }
+
+  bool ClearStopSendingFrame(const QuicFrame& frame) {
+    EXPECT_EQ(STOP_SENDING_FRAME, frame.type);
+    DeleteFrame(&const_cast<QuicFrame&>(frame));
+    return true;
+  }
+
  protected:
   MockQuicConnectionHelper helper_;
   MockAlarmFactory alarm_factory_;
@@ -1445,6 +1457,10 @@
 TEST_P(QuicParameterizedStreamTest, CheckStopSending) {
   Initialize();
   const int kStopSendingCode = 123;
+  // These must start as false.
+  EXPECT_FALSE(QuicStreamPeer::write_side_closed(stream_));
+  EXPECT_FALSE(QuicStreamPeer::read_side_closed(stream_));
+  // Expect to actually see a stop sending if and only if we are in version 99.
   if (connection_->transport_version() == QUIC_VERSION_99) {
     EXPECT_CALL(*session_, SendStopSending(kStopSendingCode, stream_->id()))
         .Times(1);
@@ -1452,6 +1468,74 @@
     EXPECT_CALL(*session_, SendStopSending(_, _)).Times(0);
   }
   stream_->SendStopSending(kStopSendingCode);
+  // Sending a STOP_SENDING does not actually close the local stream.
+  // Our implementation waits for the responding RESET_STREAM to effect the
+  // closes. Therefore, read- and write-side closes should both be false.
+  EXPECT_FALSE(QuicStreamPeer::write_side_closed(stream_));
+  EXPECT_FALSE(QuicStreamPeer::read_side_closed(stream_));
+}
+
+// Test that OnStreamReset does one-way (read) closes if version 99, two way
+// (read and write) if not version 99.
+TEST_P(QuicStreamTest, OnStreamResetReadOrReadWrite) {
+  Initialize();
+  EXPECT_FALSE(QuicStreamPeer::write_side_closed(stream_));
+  EXPECT_FALSE(QuicStreamPeer::read_side_closed(stream_));
+
+  QuicRstStreamFrame rst_frame(kInvalidControlFrameId, stream_->id(),
+                               QUIC_STREAM_CANCELLED, 1234);
+  stream_->OnStreamReset(rst_frame);
+  if (connection_->transport_version() == QUIC_VERSION_99) {
+    // Version 99/IETF QUIC should close just the read side.
+    EXPECT_TRUE(QuicStreamPeer::read_side_closed(stream_));
+    EXPECT_FALSE(QuicStreamPeer::write_side_closed(stream_));
+  } else {
+    // Google QUIC should close both sides of the stream.
+    EXPECT_TRUE(QuicStreamPeer::write_side_closed(stream_));
+    EXPECT_TRUE(QuicStreamPeer::read_side_closed(stream_));
+  }
+}
+
+// Test that receiving a STOP_SENDING just closes the write side of the stream.
+// If not V99, the test is a noop (no STOP_SENDING in Google QUIC).
+TEST_P(QuicStreamTest, OnStopSendingReadOrReadWrite) {
+  Initialize();
+  if (connection_->transport_version() != QUIC_VERSION_99) {
+    return;
+  }
+
+  EXPECT_FALSE(QuicStreamPeer::write_side_closed(stream_));
+  EXPECT_FALSE(QuicStreamPeer::read_side_closed(stream_));
+
+  // Simulate receipt of a STOP_SENDING.
+  stream_->OnStopSending(123);
+
+  // Should close just the read side.
+  EXPECT_FALSE(QuicStreamPeer::read_side_closed(stream_));
+  EXPECT_TRUE(QuicStreamPeer::write_side_closed(stream_));
+}
+
+// SendOnlyRstStream must only send a RESET_STREAM (no bundled STOP_SENDING).
+TEST_P(QuicStreamTest, SendOnlyRstStream) {
+  Initialize();
+  if (connection_->transport_version() != QUIC_VERSION_99) {
+    return;
+  }
+
+  EXPECT_CALL(*connection_,
+              OnStreamReset(stream_->id(), QUIC_BAD_APPLICATION_PAYLOAD));
+  EXPECT_CALL(*connection_, SendControlFrame(_))
+      .Times(1)
+      .WillOnce(Invoke(this, &QuicStreamTest::ClearResetStreamFrame));
+
+  QuicSessionPeer::SendRstStreamInner(session_.get(), stream_->id(),
+                                      QUIC_BAD_APPLICATION_PAYLOAD,
+                                      stream_->stream_bytes_written(),
+                                      /*close_write_side_only=*/true);
+
+  // ResetStreamOnly should just close the write side.
+  EXPECT_FALSE(QuicStreamPeer::read_side_closed(stream_));
+  EXPECT_TRUE(QuicStreamPeer::write_side_closed(stream_));
 }
 
 }  // namespace
diff --git a/net/third_party/quic/test_tools/mock_quic_session_visitor.h b/net/third_party/quic/test_tools/mock_quic_session_visitor.h
index 2c36527..450eb826 100644
--- a/net/third_party/quic/test_tools/mock_quic_session_visitor.h
+++ b/net/third_party/quic/test_tools/mock_quic_session_visitor.h
@@ -27,6 +27,7 @@
   MOCK_METHOD1(OnWriteBlocked,
                void(QuicBlockedWriterInterface* blocked_writer));
   MOCK_METHOD1(OnRstStreamReceived, void(const QuicRstStreamFrame& frame));
+  MOCK_METHOD1(OnStopSendingReceived, void(const QuicStopSendingFrame& frame));
   MOCK_METHOD1(OnConnectionAddedToTimeWaitList,
                void(QuicConnectionId connection_id));
 };
diff --git a/net/third_party/quic/test_tools/quic_session_peer.cc b/net/third_party/quic/test_tools/quic_session_peer.cc
index 22b48e8..537605ac 100644
--- a/net/third_party/quic/test_tools/quic_session_peer.cc
+++ b/net/third_party/quic/test_tools/quic_session_peer.cc
@@ -184,5 +184,14 @@
   return &session->v99_streamid_manager_.unidirectional_stream_id_manager_;
 }
 
+// static
+void QuicSessionPeer::SendRstStreamInner(QuicSession* session,
+                                         QuicStreamId id,
+                                         QuicRstStreamErrorCode error,
+                                         QuicStreamOffset bytes_written,
+                                         bool close_write_side_only) {
+  session->SendRstStreamInner(id, error, bytes_written, close_write_side_only);
+}
+
 }  // namespace test
 }  // namespace quic
diff --git a/net/third_party/quic/test_tools/quic_session_peer.h b/net/third_party/quic/test_tools/quic_session_peer.h
index eb75675..a83a1d8 100644
--- a/net/third_party/quic/test_tools/quic_session_peer.h
+++ b/net/third_party/quic/test_tools/quic_session_peer.h
@@ -67,6 +67,11 @@
       QuicSession* session);
   static QuicStreamIdManager* v99_unidirectional_stream_id_manager(
       QuicSession* session);
+  static void SendRstStreamInner(QuicSession* session,
+                                 QuicStreamId id,
+                                 QuicRstStreamErrorCode error,
+                                 QuicStreamOffset bytes_written,
+                                 bool close_write_side_only);
 };
 
 }  // namespace test
diff --git a/net/third_party/quic/test_tools/quic_stream_peer.cc b/net/third_party/quic/test_tools/quic_stream_peer.cc
index e944573..7ec755b 100644
--- a/net/third_party/quic/test_tools/quic_stream_peer.cc
+++ b/net/third_party/quic/test_tools/quic_stream_peer.cc
@@ -33,6 +33,11 @@
 }
 
 // static
+bool QuicStreamPeer::write_side_closed(QuicStream* stream) {
+  return stream->write_side_closed();
+}
+
+// static
 void QuicStreamPeer::CloseReadSide(QuicStream* stream) {
   stream->CloseReadSide();
 }
diff --git a/net/third_party/quic/test_tools/quic_stream_peer.h b/net/third_party/quic/test_tools/quic_stream_peer.h
index 407aeb7..9fe9b6a 100644
--- a/net/third_party/quic/test_tools/quic_stream_peer.h
+++ b/net/third_party/quic/test_tools/quic_stream_peer.h
@@ -27,6 +27,7 @@
   static void SetWriteSideClosed(bool value, QuicStream* stream);
   static void SetStreamBytesWritten(QuicStreamOffset stream_bytes_written,
                                     QuicStream* stream);
+  static bool write_side_closed(QuicStream* stream);
   static bool read_side_closed(QuicStream* stream);
   static void CloseReadSide(QuicStream* stream);
 
diff --git a/net/third_party/quic/test_tools/quic_test_utils.h b/net/third_party/quic/test_tools/quic_test_utils.h
index aa92bfe..e3a34a3 100644
--- a/net/third_party/quic/test_tools/quic_test_utils.h
+++ b/net/third_party/quic/test_tools/quic_test_utils.h
@@ -556,6 +556,7 @@
     QuicConnection::SendConnectivityProbingResponsePacket(peer_address);
   }
   MOCK_METHOD1(OnPathResponseFrame, bool(const QuicPathResponseFrame&));
+  MOCK_METHOD1(OnStopSendingFrame, bool(const QuicStopSendingFrame& frame));
 };
 
 class PacketSavingConnection : public MockQuicConnection {
diff --git a/net/third_party/quic/tools/quic_simple_server_session_test.cc b/net/third_party/quic/tools/quic_simple_server_session_test.cc
index 475be18..1c13ead 100644
--- a/net/third_party/quic/tools/quic_simple_server_session_test.cc
+++ b/net/third_party/quic/tools/quic_simple_server_session_test.cc
@@ -256,6 +256,25 @@
     return connection_->transport_version() == QUIC_VERSION_99;
   }
 
+  void InjectStopSending(QuicStreamId stream_id,
+                         QuicRstStreamErrorCode rst_stream_code) {
+    // Create and inject a STOP_SENDING frame. In GOOGLE QUIC, receiving a
+    // RST_STREAM frame causes a two-way close. For IETF QUIC, RST_STREAM causes
+    // a one-way close.
+    if (connection_->transport_version() != QUIC_VERSION_99) {
+      // Only needed for version 99/IETF QUIC.
+      return;
+    }
+    EXPECT_CALL(owner_, OnStopSendingReceived(_)).Times(1);
+    QuicStopSendingFrame stop_sending(
+        kInvalidControlFrameId, stream_id,
+        static_cast<QuicApplicationErrorCode>(rst_stream_code));
+    // Expect the RESET_STREAM that is generated in response to receiving a
+    // STOP_SENDING.
+    EXPECT_CALL(*connection_, OnStreamReset(stream_id, rst_stream_code));
+    session_->OnStopSendingFrame(stop_sending);
+  }
+
   StrictMock<MockQuicSessionVisitor> owner_;
   StrictMock<MockQuicCryptoServerStreamHelper> stream_helper_;
   MockQuicConnectionHelper helper_;
@@ -288,10 +307,18 @@
                           QUIC_ERROR_PROCESSING_STREAM, 0);
   EXPECT_CALL(owner_, OnRstStreamReceived(_)).Times(1);
   EXPECT_CALL(*connection_, SendControlFrame(_));
-  EXPECT_CALL(*connection_,
-              OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
-                            QUIC_RST_ACKNOWLEDGEMENT));
+  if (!IsVersion99()) {
+    // For version 99, this is covered in InjectStopSending()
+    EXPECT_CALL(*connection_,
+                OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
+                              QUIC_RST_ACKNOWLEDGEMENT));
+  }
   visitor_->OnRstStream(rst1);
+  // Create and inject a STOP_SENDING frame. In GOOGLE QUIC, receiving a
+  // RST_STREAM frame causes a two-way close. For IETF QUIC, RST_STREAM causes
+  // a one-way close.
+  InjectStopSending(GetNthClientInitiatedBidirectionalId(0),
+                    QUIC_ERROR_PROCESSING_STREAM);
   EXPECT_EQ(0u, session_->GetNumOpenIncomingStreams());
 
   // Send the same two bytes of payload in a new packet.
@@ -310,11 +337,18 @@
   EXPECT_CALL(owner_, OnRstStreamReceived(_)).Times(1);
   if (!IsVersion99()) {
     EXPECT_CALL(*connection_, SendControlFrame(_));
+    // For version 99, this is covered in InjectStopSending()
+    EXPECT_CALL(*connection_,
+                OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
+                              QUIC_RST_ACKNOWLEDGEMENT));
   }
-  EXPECT_CALL(*connection_,
-              OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
-                            QUIC_RST_ACKNOWLEDGEMENT));
   visitor_->OnRstStream(rst1);
+  // Create and inject a STOP_SENDING frame. In GOOGLE QUIC, receiving a
+  // RST_STREAM frame causes a two-way close. For IETF QUIC, RST_STREAM causes
+  // a one-way close.
+  InjectStopSending(GetNthClientInitiatedBidirectionalId(0),
+                    QUIC_ERROR_PROCESSING_STREAM);
+
   EXPECT_EQ(0u, session_->GetNumOpenIncomingStreams());
 
   // Send two bytes of payload.
@@ -344,11 +378,17 @@
   EXPECT_CALL(owner_, OnRstStreamReceived(_)).Times(1);
   if (!IsVersion99()) {
     EXPECT_CALL(*connection_, SendControlFrame(_));
+    // For version 99, this is covered in InjectStopSending()
+    EXPECT_CALL(*connection_,
+                OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
+                              QUIC_RST_ACKNOWLEDGEMENT));
   }
-  EXPECT_CALL(*connection_,
-              OnStreamReset(GetNthClientInitiatedBidirectionalId(0),
-                            QUIC_RST_ACKNOWLEDGEMENT));
   visitor_->OnRstStream(rst);
+  // Create and inject a STOP_SENDING frame. In GOOGLE QUIC, receiving a
+  // RST_STREAM frame causes a two-way close. For IETF QUIC, RST_STREAM causes
+  // a one-way close.
+  InjectStopSending(GetNthClientInitiatedBidirectionalId(0),
+                    QUIC_ERROR_PROCESSING_STREAM);
 
   // If we were tracking, we'd probably want to reject this because it's data
   // past the reset point of stream 3.  As it's a closed stream we just drop the
@@ -772,8 +812,11 @@
   // stream to be opened.
   QuicStreamId stream_got_reset = GetNthServerInitiatedUnidirectionalId(0);
   EXPECT_CALL(*connection_, SendControlFrame(_));
-  EXPECT_CALL(*connection_,
-              OnStreamReset(stream_got_reset, QUIC_RST_ACKNOWLEDGEMENT));
+  if (!IsVersion99()) {
+    // For version 99, this is covered in InjectStopSending()
+    EXPECT_CALL(*connection_,
+                OnStreamReset(stream_got_reset, QUIC_RST_ACKNOWLEDGEMENT));
+  }
   EXPECT_CALL(*session_, WriteHeadersMock(stream_to_open, _, false,
                                           QuicStream::kDefaultPriority, _));
   if (IsVersion99()) {
@@ -799,6 +842,10 @@
         QuicMaxStreamIdFrame(0, GetNthServerInitiatedUnidirectionalId(10)));
   }
   visitor_->OnRstStream(rst);
+  // Create and inject a STOP_SENDING frame. In GOOGLE QUIC, receiving a
+  // RST_STREAM frame causes a two-way close. For IETF QUIC, RST_STREAM causes
+  // a one-way close.
+  InjectStopSending(stream_got_reset, QUIC_STREAM_CANCELLED);
 }
 
 }  // namespace
diff --git a/net/third_party/quic/tools/quic_simple_server_stream_test.cc b/net/third_party/quic/tools/quic_simple_server_stream_test.cc
index a8d5fdd..63ecb8d 100644
--- a/net/third_party/quic/tools/quic_simple_server_stream_test.cc
+++ b/net/third_party/quic/tools/quic_simple_server_stream_test.cc
@@ -172,6 +172,8 @@
 
   using QuicSession::ActivateStream;
 
+  MOCK_METHOD1(OnStopSendingReceived, void(const QuicStopSendingFrame& frame));
+
   spdy::SpdyHeaderBlock original_request_headers_;
 };
 
@@ -673,7 +675,12 @@
   QuicRstStreamFrame rst_frame(kInvalidControlFrameId, stream_->id(),
                                QUIC_STREAM_CANCELLED, 1234);
   stream_->OnStreamReset(rst_frame);
-
+  if (connection_->transport_version() == QUIC_VERSION_99) {
+    // For V99 receiving a RST_STREAM causes a 1-way close; the test requires
+    // a full close. A CloseWriteSide closes the other half of the stream.
+    // Everything should then work properly.
+    stream_->CloseWriteSide();
+  }
   EXPECT_TRUE(stream_->reading_stopped());
   EXPECT_TRUE(stream_->write_side_closed());
 }