Fixes stream resumption on WINDOW_UPDATE when the send window size for a stream is negative in oghttp2.

PiperOrigin-RevId: 452286704
diff --git a/quiche/http2/adapter/nghttp2_adapter_test.cc b/quiche/http2/adapter/nghttp2_adapter_test.cc
index 9a61424..22c8f83 100644
--- a/quiche/http2/adapter/nghttp2_adapter_test.cc
+++ b/quiche/http2/adapter/nghttp2_adapter_test.cc
@@ -6793,6 +6793,96 @@
                     SpdyFrameType::RST_STREAM, SpdyFrameType::RST_STREAM}));
 }
 
+TEST(NgHttp2AdapterTest, NegativeFlowControlStreamResumption) {
+  DataSavingVisitor visitor;
+  auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);
+
+  const std::string frames =
+      TestFrameSequence()
+          .ClientPreface({{INITIAL_WINDOW_SIZE, 128u * 1024u}})
+          .WindowUpdate(0, 1 << 20)
+          .Headers(1,
+                   {{":method", "GET"},
+                    {":scheme", "https"},
+                    {":authority", "example.com"},
+                    {":path", "/this/is/request/one"}},
+                   /*fin=*/true)
+          .Serialize();
+  testing::InSequence s;
+
+  // Client preface (empty SETTINGS)
+  EXPECT_CALL(visitor, OnFrameHeader(0, 6, SETTINGS, 0));
+  EXPECT_CALL(visitor, OnSettingsStart());
+  EXPECT_CALL(visitor,
+              OnSetting(Http2Setting{Http2KnownSettingsId::INITIAL_WINDOW_SIZE,
+                                     128u * 1024u}));
+  EXPECT_CALL(visitor, OnSettingsEnd());
+
+  EXPECT_CALL(visitor, OnFrameHeader(0, 4, WINDOW_UPDATE, 0));
+  EXPECT_CALL(visitor, OnWindowUpdate(0, 1 << 20));
+
+  // Stream 1
+  EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 0x5));
+  EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, _, _)).Times(4);
+  EXPECT_CALL(visitor, OnEndHeadersForStream(1));
+  EXPECT_CALL(visitor, OnEndStream(1));
+
+  const int64_t read_result = adapter->ProcessBytes(frames);
+  EXPECT_EQ(static_cast<size_t>(read_result), frames.size());
+
+  // Submit a response for the stream.
+  auto body = absl::make_unique<TestDataFrameSource>(visitor, true);
+  TestDataFrameSource& body_ref = *body;
+  body_ref.AppendPayload(std::string(70000, 'a'));
+  int submit_result = adapter->SubmitResponse(
+      1, ToHeaders({{":status", "200"}}), std::move(body));
+  ASSERT_EQ(0, submit_result);
+
+  EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1));
+  EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0));
+  EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4));
+  EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0));
+  EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0)).Times(5);
+
+  adapter->Send();
+  EXPECT_FALSE(adapter->want_write());
+
+  EXPECT_CALL(visitor, OnFrameHeader(0, 6, SETTINGS, 0));
+  EXPECT_CALL(visitor, OnSettingsStart());
+  EXPECT_CALL(visitor,
+              OnSetting(Http2Setting{Http2KnownSettingsId::INITIAL_WINDOW_SIZE,
+                                     64u * 1024u}));
+  EXPECT_CALL(visitor, OnSettingsEnd());
+
+  // Processing these SETTINGS will cause stream 1's send window to become
+  // negative.
+  adapter->ProcessBytes(TestFrameSequence()
+                            .Settings({{INITIAL_WINDOW_SIZE, 64u * 1024u}})
+                            .Serialize());
+  EXPECT_TRUE(adapter->want_write());
+  // nghttp2 does not expose the fact that the send window size is negative.
+  EXPECT_EQ(adapter->GetStreamSendWindowSize(1), 0);
+
+  body_ref.AppendPayload("Stream should be resumed.");
+  adapter->ResumeStream(1);
+
+  EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1));
+  EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0));
+  adapter->Send();
+  EXPECT_FALSE(adapter->want_write());
+
+  // Upon receiving the WINDOW_UPDATE, stream 1 should be ready to write.
+  EXPECT_CALL(visitor, OnFrameHeader(1, 4, WINDOW_UPDATE, 0));
+  EXPECT_CALL(visitor, OnWindowUpdate(1, 10000));
+  adapter->ProcessBytes(TestFrameSequence().WindowUpdate(1, 10000).Serialize());
+  EXPECT_TRUE(adapter->want_write());
+  EXPECT_GT(adapter->GetStreamSendWindowSize(1), 0);
+
+  EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0));
+  adapter->Send();
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace adapter
diff --git a/quiche/http2/adapter/oghttp2_adapter_test.cc b/quiche/http2/adapter/oghttp2_adapter_test.cc
index 077e33c..f09867c 100644
--- a/quiche/http2/adapter/oghttp2_adapter_test.cc
+++ b/quiche/http2/adapter/oghttp2_adapter_test.cc
@@ -7556,6 +7556,100 @@
   EXPECT_EQ(frames.size(), static_cast<size_t>(result));
 }
 
+TEST(OgHttp2AdapterTest, NegativeFlowControlStreamResumption) {
+  DataSavingVisitor visitor;
+  OgHttp2Adapter::Options options;
+  options.perspective = Perspective::kServer;
+  auto adapter = OgHttp2Adapter::Create(visitor, options);
+
+  const std::string frames =
+      TestFrameSequence()
+          .ClientPreface({{INITIAL_WINDOW_SIZE, 128u * 1024u}})
+          .WindowUpdate(0, 1 << 20)
+          .Headers(1,
+                   {{":method", "GET"},
+                    {":scheme", "https"},
+                    {":authority", "example.com"},
+                    {":path", "/this/is/request/one"}},
+                   /*fin=*/true)
+          .Serialize();
+  testing::InSequence s;
+
+  // Client preface (empty SETTINGS)
+  EXPECT_CALL(visitor, OnFrameHeader(0, 6, SETTINGS, 0));
+  EXPECT_CALL(visitor, OnSettingsStart());
+  EXPECT_CALL(visitor,
+              OnSetting(Http2Setting{Http2KnownSettingsId::INITIAL_WINDOW_SIZE,
+                                     128u * 1024u}));
+  EXPECT_CALL(visitor, OnSettingsEnd());
+
+  EXPECT_CALL(visitor, OnFrameHeader(0, 4, WINDOW_UPDATE, 0));
+  EXPECT_CALL(visitor, OnWindowUpdate(0, 1 << 20));
+
+  // Stream 1
+  EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 0x5));
+  EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, _, _)).Times(4);
+  EXPECT_CALL(visitor, OnEndHeadersForStream(1));
+  EXPECT_CALL(visitor, OnEndStream(1));
+
+  const int64_t read_result = adapter->ProcessBytes(frames);
+  EXPECT_EQ(static_cast<size_t>(read_result), frames.size());
+
+  // Submit a response for the stream.
+  auto body = absl::make_unique<TestDataFrameSource>(visitor, true);
+  TestDataFrameSource& body_ref = *body;
+  body_ref.AppendPayload(std::string(70000, 'a'));
+  int submit_result = adapter->SubmitResponse(
+      1, ToHeaders({{":status", "200"}}), std::move(body));
+  ASSERT_EQ(0, submit_result);
+
+  EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0));
+  EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0));
+
+  EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1));
+  EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0));
+  EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4));
+  EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0));
+  EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0)).Times(5);
+
+  adapter->Send();
+  EXPECT_FALSE(adapter->want_write());
+
+  EXPECT_CALL(visitor, OnFrameHeader(0, 6, SETTINGS, 0));
+  EXPECT_CALL(visitor, OnSettingsStart());
+  EXPECT_CALL(visitor,
+              OnSetting(Http2Setting{Http2KnownSettingsId::INITIAL_WINDOW_SIZE,
+                                     64u * 1024u}));
+  EXPECT_CALL(visitor, OnSettingsEnd());
+
+  // Processing these SETTINGS will cause stream 1's send window to become
+  // negative.
+  adapter->ProcessBytes(TestFrameSequence()
+                            .Settings({{INITIAL_WINDOW_SIZE, 64u * 1024u}})
+                            .Serialize());
+  EXPECT_TRUE(adapter->want_write());
+  EXPECT_LT(adapter->GetStreamSendWindowSize(1), 0);
+
+  body_ref.AppendPayload("Stream should be resumed.");
+  adapter->ResumeStream(1);
+
+  EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1));
+  EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0));
+  adapter->Send();
+  EXPECT_FALSE(adapter->want_write());
+
+  // Upon receiving the WINDOW_UPDATE, stream 1 should be ready to write.
+  EXPECT_CALL(visitor, OnFrameHeader(1, 4, WINDOW_UPDATE, 0));
+  EXPECT_CALL(visitor, OnWindowUpdate(1, 10000));
+  adapter->ProcessBytes(TestFrameSequence().WindowUpdate(1, 10000).Serialize());
+  EXPECT_TRUE(adapter->want_write());
+  EXPECT_GT(adapter->GetStreamSendWindowSize(1), 0);
+
+  EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0));
+  adapter->Send();
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace adapter
diff --git a/quiche/http2/adapter/oghttp2_session.cc b/quiche/http2/adapter/oghttp2_session.cc
index 6cbe736..0f0d6bd 100644
--- a/quiche/http2/adapter/oghttp2_session.cc
+++ b/quiche/http2/adapter/oghttp2_session.cc
@@ -1416,12 +1416,13 @@
       if (streams_reset_.contains(stream_id)) {
         return;
       }
-      if (it->second.send_window == 0) {
+      const bool was_blocked = (it->second.send_window <= 0);
+      it->second.send_window += delta_window_size;
+      if (was_blocked && it->second.send_window > 0) {
         // The stream was blocked on flow control.
         QUICHE_VLOG(1) << "Marking stream " << stream_id << " ready to write.";
         write_scheduler_.MarkStreamReady(stream_id, false);
       }
-      it->second.send_window += delta_window_size;
     }
   }
   visitor_.OnWindowUpdate(stream_id, delta_window_size);