Wrap OgHttp2Session callbacks with a latched_error_ check.

This CL introduces a new class EventForwarder that exists mostly to forward
SpdyFramerVisitorInterface events to its OgHttp2Session receiver. However, if
OgHttp2Session has encountered a connection-level error and called
LatchErrorAndNotify(), then the EventForwarder drops the event instead of
forwarding to OgHttp2Session. This functionality is analogous to
Http2Dispatcher::SpdyFramerAdaptor:
http://google3/gfe/gfe2/http2/http2_dispatcher.h;l=975;rcl=406920946.

The immediate use case is to prevent calls to OnStreamEnd() after header
validation indicates a connection-level error, where OnStreamEnd() would result
in further (unwanted) processing on the connection. Another future potential
use case could be to prevent subsequent OnSetting() calls after one OnSetting()
call results in a connection-level error (e.g., future SETTINGS validation).

PiperOrigin-RevId: 407352242
diff --git a/http2/adapter/event_forwarder.cc b/http2/adapter/event_forwarder.cc
new file mode 100644
index 0000000..e4b1f3d
--- /dev/null
+++ b/http2/adapter/event_forwarder.cc
@@ -0,0 +1,181 @@
+#include "http2/adapter/event_forwarder.h"
+
+namespace http2 {
+namespace adapter {
+
+EventForwarder::EventForwarder(ForwardPredicate can_forward,
+                               spdy::SpdyFramerVisitorInterface& receiver)
+    : can_forward_(std::move(can_forward)), receiver_(receiver) {}
+
+void EventForwarder::OnError(Http2DecoderAdapter::SpdyFramerError error,
+                             std::string detailed_error) {
+  if (can_forward_()) {
+    receiver_.OnError(error, std::move(detailed_error));
+  }
+}
+
+void EventForwarder::OnCommonHeader(spdy::SpdyStreamId stream_id, size_t length,
+                                    uint8_t type, uint8_t flags) {
+  if (can_forward_()) {
+    receiver_.OnCommonHeader(stream_id, length, type, flags);
+  }
+}
+
+void EventForwarder::OnDataFrameHeader(spdy::SpdyStreamId stream_id,
+                                       size_t length, bool fin) {
+  if (can_forward_()) {
+    receiver_.OnDataFrameHeader(stream_id, length, fin);
+  }
+}
+
+void EventForwarder::OnStreamFrameData(spdy::SpdyStreamId stream_id,
+                                       const char* data, size_t len) {
+  if (can_forward_()) {
+    receiver_.OnStreamFrameData(stream_id, data, len);
+  }
+}
+
+void EventForwarder::OnStreamEnd(spdy::SpdyStreamId stream_id) {
+  if (can_forward_()) {
+    receiver_.OnStreamEnd(stream_id);
+  }
+}
+
+void EventForwarder::OnStreamPadLength(spdy::SpdyStreamId stream_id,
+                                       size_t value) {
+  if (can_forward_()) {
+    receiver_.OnStreamPadLength(stream_id, value);
+  }
+}
+
+void EventForwarder::OnStreamPadding(spdy::SpdyStreamId stream_id, size_t len) {
+  if (can_forward_()) {
+    receiver_.OnStreamPadding(stream_id, len);
+  }
+}
+
+spdy::SpdyHeadersHandlerInterface* EventForwarder::OnHeaderFrameStart(
+    spdy::SpdyStreamId stream_id) {
+  return receiver_.OnHeaderFrameStart(stream_id);
+}
+
+void EventForwarder::OnHeaderFrameEnd(spdy::SpdyStreamId stream_id) {
+  if (can_forward_()) {
+    receiver_.OnHeaderFrameEnd(stream_id);
+  }
+}
+
+void EventForwarder::OnRstStream(spdy::SpdyStreamId stream_id,
+                                 spdy::SpdyErrorCode error_code) {
+  if (can_forward_()) {
+    receiver_.OnRstStream(stream_id, error_code);
+  }
+}
+
+void EventForwarder::OnSettings() {
+  if (can_forward_()) {
+    receiver_.OnSettings();
+  }
+}
+
+void EventForwarder::OnSetting(spdy::SpdySettingsId id, uint32_t value) {
+  if (can_forward_()) {
+    receiver_.OnSetting(id, value);
+  }
+}
+
+void EventForwarder::OnSettingsEnd() {
+  if (can_forward_()) {
+    receiver_.OnSettingsEnd();
+  }
+}
+
+void EventForwarder::OnSettingsAck() {
+  if (can_forward_()) {
+    receiver_.OnSettingsAck();
+  }
+}
+
+void EventForwarder::OnPing(spdy::SpdyPingId unique_id, bool is_ack) {
+  if (can_forward_()) {
+    receiver_.OnPing(unique_id, is_ack);
+  }
+}
+
+void EventForwarder::OnGoAway(spdy::SpdyStreamId last_accepted_stream_id,
+                              spdy::SpdyErrorCode error_code) {
+  if (can_forward_()) {
+    receiver_.OnGoAway(last_accepted_stream_id, error_code);
+  }
+}
+
+bool EventForwarder::OnGoAwayFrameData(const char* goaway_data, size_t len) {
+  if (can_forward_()) {
+    return receiver_.OnGoAwayFrameData(goaway_data, len);
+  }
+  return false;
+}
+
+void EventForwarder::OnHeaders(spdy::SpdyStreamId stream_id, bool has_priority,
+                               int weight, spdy::SpdyStreamId parent_stream_id,
+                               bool exclusive, bool fin, bool end) {
+  if (can_forward_()) {
+    receiver_.OnHeaders(stream_id, has_priority, weight, parent_stream_id,
+                        exclusive, fin, end);
+  }
+}
+
+void EventForwarder::OnWindowUpdate(spdy::SpdyStreamId stream_id,
+                                    int delta_window_size) {
+  if (can_forward_()) {
+    receiver_.OnWindowUpdate(stream_id, delta_window_size);
+  }
+}
+
+void EventForwarder::OnPushPromise(spdy::SpdyStreamId stream_id,
+                                   spdy::SpdyStreamId promised_stream_id,
+                                   bool end) {
+  if (can_forward_()) {
+    receiver_.OnPushPromise(stream_id, promised_stream_id, end);
+  }
+}
+
+void EventForwarder::OnContinuation(spdy::SpdyStreamId stream_id, bool end) {
+  if (can_forward_()) {
+    receiver_.OnContinuation(stream_id, end);
+  }
+}
+
+void EventForwarder::OnAltSvc(
+    spdy::SpdyStreamId stream_id, absl::string_view origin,
+    const spdy::SpdyAltSvcWireFormat::AlternativeServiceVector& altsvc_vector) {
+  if (can_forward_()) {
+    receiver_.OnAltSvc(stream_id, origin, altsvc_vector);
+  }
+}
+
+void EventForwarder::OnPriority(spdy::SpdyStreamId stream_id,
+                                spdy::SpdyStreamId parent_stream_id, int weight,
+                                bool exclusive) {
+  if (can_forward_()) {
+    receiver_.OnPriority(stream_id, parent_stream_id, weight, exclusive);
+  }
+}
+
+void EventForwarder::OnPriorityUpdate(spdy::SpdyStreamId prioritized_stream_id,
+                                      absl::string_view priority_field_value) {
+  if (can_forward_()) {
+    receiver_.OnPriorityUpdate(prioritized_stream_id, priority_field_value);
+  }
+}
+
+bool EventForwarder::OnUnknownFrame(spdy::SpdyStreamId stream_id,
+                                    uint8_t frame_type) {
+  if (can_forward_()) {
+    return receiver_.OnUnknownFrame(stream_id, frame_type);
+  }
+  return false;
+}
+
+}  // namespace adapter
+}  // namespace http2
diff --git a/http2/adapter/event_forwarder.h b/http2/adapter/event_forwarder.h
new file mode 100644
index 0000000..d4dce1e
--- /dev/null
+++ b/http2/adapter/event_forwarder.h
@@ -0,0 +1,76 @@
+#ifndef QUICHE_HTTP2_ADAPTER_EVENT_FORWARDER_H_
+#define QUICHE_HTTP2_ADAPTER_EVENT_FORWARDER_H_
+
+#include <functional>
+
+#include "common/platform/api/quiche_export.h"
+#include "spdy/core/http2_frame_decoder_adapter.h"
+
+namespace http2 {
+namespace adapter {
+
+// Forwards events to a provided SpdyFramerVisitorInterface receiver if the
+// provided predicate succeeds. Currently, OnHeaderFrameStart() is always
+// forwarded regardless of the predicate.
+// TODO(diannahu): Add a NoOpHeadersHandler if needed.
+class QUICHE_EXPORT_PRIVATE EventForwarder
+    : public spdy::SpdyFramerVisitorInterface {
+ public:
+  // Whether the forwarder can forward events to the receiver.
+  using ForwardPredicate = std::function<bool()>;
+
+  EventForwarder(ForwardPredicate can_forward,
+                 spdy::SpdyFramerVisitorInterface& receiver);
+
+  void OnError(Http2DecoderAdapter::SpdyFramerError error,
+               std::string detailed_error) override;
+  void OnCommonHeader(spdy::SpdyStreamId stream_id, size_t length, uint8_t type,
+                      uint8_t flags) override;
+  void OnDataFrameHeader(spdy::SpdyStreamId stream_id, size_t length,
+                         bool fin) override;
+  void OnStreamFrameData(spdy::SpdyStreamId stream_id, const char* data,
+                         size_t len) override;
+  void OnStreamEnd(spdy::SpdyStreamId stream_id) override;
+  void OnStreamPadLength(spdy::SpdyStreamId stream_id, size_t value) override;
+  void OnStreamPadding(spdy::SpdyStreamId stream_id, size_t len) override;
+  spdy::SpdyHeadersHandlerInterface* OnHeaderFrameStart(
+      spdy::SpdyStreamId stream_id) override;
+  void OnHeaderFrameEnd(spdy::SpdyStreamId stream_id) override;
+  void OnRstStream(spdy::SpdyStreamId stream_id,
+                   spdy::SpdyErrorCode error_code) override;
+  void OnSettings() override;
+  void OnSetting(spdy::SpdySettingsId id, uint32_t value) override;
+  void OnSettingsEnd() override;
+  void OnSettingsAck() override;
+  void OnPing(spdy::SpdyPingId unique_id, bool is_ack) override;
+  void OnGoAway(spdy::SpdyStreamId last_accepted_stream_id,
+                spdy::SpdyErrorCode error_code) override;
+  bool OnGoAwayFrameData(const char* goaway_data, size_t len) override;
+  void OnHeaders(spdy::SpdyStreamId stream_id, bool has_priority, int weight,
+                 spdy::SpdyStreamId parent_stream_id, bool exclusive, bool fin,
+                 bool end) override;
+  void OnWindowUpdate(spdy::SpdyStreamId stream_id,
+                      int delta_window_size) override;
+  void OnPushPromise(spdy::SpdyStreamId stream_id,
+                     spdy::SpdyStreamId promised_stream_id, bool end) override;
+  void OnContinuation(spdy::SpdyStreamId stream_id, bool end) override;
+  void OnAltSvc(spdy::SpdyStreamId stream_id, absl::string_view origin,
+                const spdy::SpdyAltSvcWireFormat::AlternativeServiceVector&
+                    altsvc_vector) override;
+  void OnPriority(spdy::SpdyStreamId stream_id,
+                  spdy::SpdyStreamId parent_stream_id, int weight,
+                  bool exclusive) override;
+  void OnPriorityUpdate(spdy::SpdyStreamId prioritized_stream_id,
+                        absl::string_view priority_field_value) override;
+  bool OnUnknownFrame(spdy::SpdyStreamId stream_id,
+                      uint8_t frame_type) override;
+
+ private:
+  ForwardPredicate can_forward_;
+  spdy::SpdyFramerVisitorInterface& receiver_;
+};
+
+}  // namespace adapter
+}  // namespace http2
+
+#endif  // QUICHE_HTTP2_ADAPTER_EVENT_FORWARDER_H_
diff --git a/http2/adapter/event_forwarder_test.cc b/http2/adapter/event_forwarder_test.cc
new file mode 100644
index 0000000..3374f5e
--- /dev/null
+++ b/http2/adapter/event_forwarder_test.cc
@@ -0,0 +1,220 @@
+#include "http2/adapter/event_forwarder.h"
+
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "common/platform/api/quiche_test.h"
+#include "spdy/core/mock_spdy_framer_visitor.h"
+#include "spdy/core/spdy_protocol.h"
+
+namespace http2 {
+namespace adapter {
+namespace test {
+namespace {
+
+constexpr absl::string_view some_data = "Here is some data for events";
+constexpr spdy::SpdyStreamId stream_id = 1;
+constexpr spdy::SpdyErrorCode error_code =
+    spdy::SpdyErrorCode::ERROR_CODE_ENHANCE_YOUR_CALM;
+constexpr size_t length = 42;
+
+TEST(EventForwarderTest, ForwardsEventsWithTruePredicate) {
+  spdy::test::MockSpdyFramerVisitor receiver;
+  receiver.DelegateHeaderHandling();
+  EventForwarder event_forwarder([]() { return true; }, receiver);
+
+  EXPECT_CALL(
+      receiver,
+      OnError(Http2DecoderAdapter::SpdyFramerError::SPDY_STOP_PROCESSING,
+              std::string(some_data)));
+  event_forwarder.OnError(
+      Http2DecoderAdapter::SpdyFramerError::SPDY_STOP_PROCESSING,
+      std::string(some_data));
+
+  EXPECT_CALL(receiver,
+              OnCommonHeader(stream_id, length, /*type=*/0x0, /*flags=*/0x1));
+  event_forwarder.OnCommonHeader(stream_id, length, /*type=*/0x0,
+                                 /*flags=*/0x1);
+
+  EXPECT_CALL(receiver, OnDataFrameHeader(stream_id, length, /*fin=*/true));
+  event_forwarder.OnDataFrameHeader(stream_id, length, /*fin=*/true);
+
+  EXPECT_CALL(receiver,
+              OnStreamFrameData(stream_id, some_data.data(), some_data.size()));
+  event_forwarder.OnStreamFrameData(stream_id, some_data.data(),
+                                    some_data.size());
+
+  EXPECT_CALL(receiver, OnStreamEnd(stream_id));
+  event_forwarder.OnStreamEnd(stream_id);
+
+  EXPECT_CALL(receiver, OnStreamPadLength(stream_id, length));
+  event_forwarder.OnStreamPadLength(stream_id, length);
+
+  EXPECT_CALL(receiver, OnStreamPadding(stream_id, length));
+  event_forwarder.OnStreamPadding(stream_id, length);
+
+  EXPECT_CALL(receiver, OnHeaderFrameStart(stream_id));
+  spdy::SpdyHeadersHandlerInterface* handler =
+      event_forwarder.OnHeaderFrameStart(stream_id);
+  EXPECT_EQ(handler, receiver.ReturnTestHeadersHandler(stream_id));
+
+  EXPECT_CALL(receiver, OnHeaderFrameEnd(stream_id));
+  event_forwarder.OnHeaderFrameEnd(stream_id);
+
+  EXPECT_CALL(receiver, OnRstStream(stream_id, error_code));
+  event_forwarder.OnRstStream(stream_id, error_code);
+
+  EXPECT_CALL(receiver, OnSettings());
+  event_forwarder.OnSettings();
+
+  EXPECT_CALL(
+      receiver,
+      OnSetting(spdy::SpdyKnownSettingsId::SETTINGS_MAX_CONCURRENT_STREAMS,
+                100));
+  event_forwarder.OnSetting(
+      spdy::SpdyKnownSettingsId::SETTINGS_MAX_CONCURRENT_STREAMS, 100);
+
+  EXPECT_CALL(receiver, OnSettingsEnd());
+  event_forwarder.OnSettingsEnd();
+
+  EXPECT_CALL(receiver, OnSettingsAck());
+  event_forwarder.OnSettingsAck();
+
+  EXPECT_CALL(receiver, OnPing(/*unique_id=*/42, /*is_ack=*/false));
+  event_forwarder.OnPing(/*unique_id=*/42, /*is_ack=*/false);
+
+  EXPECT_CALL(receiver, OnGoAway(stream_id, error_code));
+  event_forwarder.OnGoAway(stream_id, error_code);
+
+  EXPECT_CALL(receiver, OnGoAwayFrameData(some_data.data(), some_data.size()));
+  event_forwarder.OnGoAwayFrameData(some_data.data(), some_data.size());
+
+  EXPECT_CALL(
+      receiver,
+      OnHeaders(stream_id, /*has_priority=*/false, /*weight=*/42, stream_id + 2,
+                /*exclusive=*/false, /*fin=*/true, /*end=*/true));
+  event_forwarder.OnHeaders(stream_id, /*has_priority=*/false, /*weight=*/42,
+                            stream_id + 2, /*exclusive=*/false, /*fin=*/true,
+                            /*end=*/true);
+
+  EXPECT_CALL(receiver, OnWindowUpdate(stream_id, /*delta_window_size=*/42));
+  event_forwarder.OnWindowUpdate(stream_id, /*delta_window_size=*/42);
+
+  EXPECT_CALL(receiver, OnPushPromise(stream_id, stream_id + 1, /*end=*/true));
+  event_forwarder.OnPushPromise(stream_id, stream_id + 1, /*end=*/true);
+
+  EXPECT_CALL(receiver, OnContinuation(stream_id, /*end=*/true));
+  event_forwarder.OnContinuation(stream_id, /*end=*/true);
+
+  const spdy::SpdyAltSvcWireFormat::AlternativeServiceVector altsvc_vector;
+  EXPECT_CALL(receiver, OnAltSvc(stream_id, some_data, altsvc_vector));
+  event_forwarder.OnAltSvc(stream_id, some_data, altsvc_vector);
+
+  EXPECT_CALL(receiver, OnPriority(stream_id, stream_id + 2, /*weight=*/42,
+                                   /*exclusive=*/false));
+  event_forwarder.OnPriority(stream_id, stream_id + 2, /*weight=*/42,
+                             /*exclusive=*/false);
+
+  EXPECT_CALL(receiver, OnPriorityUpdate(stream_id, some_data));
+  event_forwarder.OnPriorityUpdate(stream_id, some_data);
+
+  EXPECT_CALL(receiver, OnUnknownFrame(stream_id, /*frame_type=*/0x4D));
+  event_forwarder.OnUnknownFrame(stream_id, /*frame_type=*/0x4D);
+}
+
+TEST(EventForwarderTest, DoesNotForwardEventsWithFalsePredicate) {
+  spdy::test::MockSpdyFramerVisitor receiver;
+  receiver.DelegateHeaderHandling();
+  EventForwarder event_forwarder([]() { return false; }, receiver);
+
+  EXPECT_CALL(receiver, OnError).Times(0);
+  event_forwarder.OnError(
+      Http2DecoderAdapter::SpdyFramerError::SPDY_STOP_PROCESSING,
+      std::string(some_data));
+
+  EXPECT_CALL(receiver, OnCommonHeader).Times(0);
+  event_forwarder.OnCommonHeader(stream_id, length, /*type=*/0x0,
+                                 /*flags=*/0x1);
+
+  EXPECT_CALL(receiver, OnDataFrameHeader).Times(0);
+  event_forwarder.OnDataFrameHeader(stream_id, length, /*fin=*/true);
+
+  EXPECT_CALL(receiver, OnStreamFrameData).Times(0);
+  event_forwarder.OnStreamFrameData(stream_id, some_data.data(),
+                                    some_data.size());
+
+  EXPECT_CALL(receiver, OnStreamEnd).Times(0);
+  event_forwarder.OnStreamEnd(stream_id);
+
+  EXPECT_CALL(receiver, OnStreamPadLength).Times(0);
+  event_forwarder.OnStreamPadLength(stream_id, length);
+
+  EXPECT_CALL(receiver, OnStreamPadding).Times(0);
+  event_forwarder.OnStreamPadding(stream_id, length);
+
+  EXPECT_CALL(receiver, OnHeaderFrameStart(stream_id));
+  spdy::SpdyHeadersHandlerInterface* handler =
+      event_forwarder.OnHeaderFrameStart(stream_id);
+  EXPECT_EQ(handler, receiver.ReturnTestHeadersHandler(stream_id));
+
+  EXPECT_CALL(receiver, OnHeaderFrameEnd).Times(0);
+  event_forwarder.OnHeaderFrameEnd(stream_id);
+
+  EXPECT_CALL(receiver, OnRstStream).Times(0);
+  event_forwarder.OnRstStream(stream_id, error_code);
+
+  EXPECT_CALL(receiver, OnSettings).Times(0);
+  event_forwarder.OnSettings();
+
+  EXPECT_CALL(receiver, OnSetting).Times(0);
+  event_forwarder.OnSetting(
+      spdy::SpdyKnownSettingsId::SETTINGS_MAX_CONCURRENT_STREAMS, 100);
+
+  EXPECT_CALL(receiver, OnSettingsEnd).Times(0);
+  event_forwarder.OnSettingsEnd();
+
+  EXPECT_CALL(receiver, OnSettingsAck).Times(0);
+  event_forwarder.OnSettingsAck();
+
+  EXPECT_CALL(receiver, OnPing).Times(0);
+  event_forwarder.OnPing(/*unique_id=*/42, /*is_ack=*/false);
+
+  EXPECT_CALL(receiver, OnGoAway).Times(0);
+  event_forwarder.OnGoAway(stream_id, error_code);
+
+  EXPECT_CALL(receiver, OnGoAwayFrameData).Times(0);
+  event_forwarder.OnGoAwayFrameData(some_data.data(), some_data.size());
+
+  EXPECT_CALL(receiver, OnHeaders).Times(0);
+  event_forwarder.OnHeaders(stream_id, /*has_priority=*/false, /*weight=*/42,
+                            stream_id + 2, /*exclusive=*/false, /*fin=*/true,
+                            /*end=*/true);
+
+  EXPECT_CALL(receiver, OnWindowUpdate).Times(0);
+  event_forwarder.OnWindowUpdate(stream_id, /*delta_window_size=*/42);
+
+  EXPECT_CALL(receiver, OnPushPromise).Times(0);
+  event_forwarder.OnPushPromise(stream_id, stream_id + 1, /*end=*/true);
+
+  EXPECT_CALL(receiver, OnContinuation).Times(0);
+  event_forwarder.OnContinuation(stream_id, /*end=*/true);
+
+  EXPECT_CALL(receiver, OnAltSvc).Times(0);
+  const spdy::SpdyAltSvcWireFormat::AlternativeServiceVector altsvc_vector;
+  event_forwarder.OnAltSvc(stream_id, some_data, altsvc_vector);
+
+  EXPECT_CALL(receiver, OnPriority).Times(0);
+  event_forwarder.OnPriority(stream_id, stream_id + 2, /*weight=*/42,
+                             /*exclusive=*/false);
+
+  EXPECT_CALL(receiver, OnPriorityUpdate).Times(0);
+  event_forwarder.OnPriorityUpdate(stream_id, some_data);
+
+  EXPECT_CALL(receiver, OnUnknownFrame).Times(0);
+  event_forwarder.OnUnknownFrame(stream_id, /*frame_type=*/0x4D);
+}
+
+}  // namespace
+}  // namespace test
+}  // namespace adapter
+}  // namespace http2
diff --git a/http2/adapter/nghttp2_adapter_test.cc b/http2/adapter/nghttp2_adapter_test.cc
index 4754050..6dd180e 100644
--- a/http2/adapter/nghttp2_adapter_test.cc
+++ b/http2/adapter/nghttp2_adapter_test.cc
@@ -723,6 +723,73 @@
   EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS}));
 }
 
+TEST(NgHttp2AdapterTest, ClientConnectionErrorWhileHandlingHeadersOnly) {
+  DataSavingVisitor visitor;
+  auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor);
+
+  testing::InSequence s;
+
+  const std::vector<const Header> headers1 =
+      ToHeaders({{":method", "GET"},
+                 {":scheme", "http"},
+                 {":authority", "example.com"},
+                 {":path", "/this/is/request/one"}});
+
+  const char* kSentinel1 = "arbitrary pointer 1";
+  const int32_t stream_id1 =
+      adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1));
+  ASSERT_GT(stream_id1, 0);
+  QUICHE_LOG(INFO) << "Created stream: " << stream_id1;
+
+  EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5));
+  EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0));
+
+  int result = adapter->Send();
+  EXPECT_EQ(0, result);
+  absl::string_view data = visitor.data();
+  EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix));
+  data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix));
+  EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::HEADERS}));
+  visitor.Clear();
+
+  const std::string stream_frames =
+      TestFrameSequence()
+          .ServerPreface()
+          .Headers(1,
+                   {{":status", "200"},
+                    {"server", "my-fake-server"},
+                    {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}},
+                   /*fin=*/true)
+          .Serialize();
+
+  // Server preface (empty SETTINGS)
+  EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+  EXPECT_CALL(visitor, OnSettingsStart());
+  EXPECT_CALL(visitor, OnSettingsEnd());
+
+  EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5));
+  EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server"));
+  EXPECT_CALL(visitor,
+              OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT"))
+      .WillOnce(
+          testing::Return(Http2VisitorInterface::HEADER_CONNECTION_ERROR));
+  // Translation to nghttp2 treats this error as a general parsing error.
+  EXPECT_CALL(visitor, OnConnectionError(ConnectionError::kParseError));
+
+  const int64_t stream_result = adapter->ProcessBytes(stream_frames);
+  EXPECT_EQ(-902 /* NGHTTP2_ERR_CALLBACK_FAILURE */, stream_result);
+
+  EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1));
+  EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0));
+
+  EXPECT_TRUE(adapter->want_write());
+  result = adapter->Send();
+  EXPECT_EQ(0, result);
+  EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS}));
+}
+
 TEST(NgHttp2AdapterTest, ClientRejectsHeaders) {
   DataSavingVisitor visitor;
   auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor);
diff --git a/http2/adapter/oghttp2_adapter_test.cc b/http2/adapter/oghttp2_adapter_test.cc
index 2fe13fd..05ee09d 100644
--- a/http2/adapter/oghttp2_adapter_test.cc
+++ b/http2/adapter/oghttp2_adapter_test.cc
@@ -588,6 +588,76 @@
   EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS}));
 }
 
+TEST(OgHttp2AdapterClientTest, ClientConnectionErrorWhileHandlingHeadersOnly) {
+  DataSavingVisitor visitor;
+  OgHttp2Adapter::Options options{.perspective = Perspective::kClient};
+  auto adapter = OgHttp2Adapter::Create(visitor, options);
+
+  testing::InSequence s;
+
+  const std::vector<const Header> headers1 =
+      ToHeaders({{":method", "GET"},
+                 {":scheme", "http"},
+                 {":authority", "example.com"},
+                 {":path", "/this/is/request/one"}});
+
+  const char* kSentinel1 = "arbitrary pointer 1";
+  const int32_t stream_id1 =
+      adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1));
+  ASSERT_GT(stream_id1, 0);
+  QUICHE_LOG(INFO) << "Created stream: " << stream_id1;
+
+  EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0));
+  EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0));
+  EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5));
+  EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0));
+
+  int result = adapter->Send();
+  EXPECT_EQ(0, result);
+  absl::string_view data = visitor.data();
+  EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix));
+  data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix));
+  EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::SETTINGS,
+                                  spdy::SpdyFrameType::HEADERS}));
+  visitor.Clear();
+
+  const std::string stream_frames =
+      TestFrameSequence()
+          .ServerPreface()
+          .Headers(1,
+                   {{":status", "200"},
+                    {"server", "my-fake-server"},
+                    {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}},
+                   /*fin=*/true)
+          .Serialize();
+
+  // Server preface (empty SETTINGS)
+  EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+  EXPECT_CALL(visitor, OnSettingsStart());
+  EXPECT_CALL(visitor, OnSettingsEnd());
+
+  EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5));
+  EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server"));
+  EXPECT_CALL(visitor,
+              OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT"))
+      .WillOnce(
+          testing::Return(Http2VisitorInterface::HEADER_CONNECTION_ERROR));
+  EXPECT_CALL(visitor, OnConnectionError(ConnectionError::kHeaderError));
+
+  const int64_t stream_result = adapter->ProcessBytes(stream_frames);
+  EXPECT_LT(stream_result, 0);
+
+  EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1));
+  EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0));
+
+  EXPECT_TRUE(adapter->want_write());
+  result = adapter->Send();
+  EXPECT_EQ(0, result);
+  EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS}));
+}
+
 TEST(OgHttp2AdapterClientTest, ClientRejectsHeaders) {
   DataSavingVisitor visitor;
   OgHttp2Adapter::Options options{.perspective = Perspective::kClient};
diff --git a/http2/adapter/oghttp2_session.cc b/http2/adapter/oghttp2_session.cc
index fe84230..ed16f04 100644
--- a/http2/adapter/oghttp2_session.cc
+++ b/http2/adapter/oghttp2_session.cc
@@ -235,8 +235,9 @@
 
 OgHttp2Session::OgHttp2Session(Http2VisitorInterface& visitor, Options options)
     : visitor_(visitor),
+      event_forwarder_([this]() { return !latched_error_; }, *this),
       receive_logger_(
-          this, TracePerspectiveAsString(options.perspective),
+          &event_forwarder_, TracePerspectiveAsString(options.perspective),
           []() { return kTraceLoggingEnabled; }, this),
       send_logger_(
           TracePerspectiveAsString(options.perspective),
diff --git a/http2/adapter/oghttp2_session.h b/http2/adapter/oghttp2_session.h
index 3baf7dd..6c844d8 100644
--- a/http2/adapter/oghttp2_session.h
+++ b/http2/adapter/oghttp2_session.h
@@ -6,6 +6,7 @@
 
 #include "absl/strings/string_view.h"
 #include "http2/adapter/data_source.h"
+#include "http2/adapter/event_forwarder.h"
 #include "http2/adapter/header_validator.h"
 #include "http2/adapter/http2_protocol.h"
 #include "http2/adapter/http2_session.h"
@@ -294,6 +295,9 @@
   // Receives events when inbound frames are parsed.
   Http2VisitorInterface& visitor_;
 
+  // Forwards received events to the session if it can accept them.
+  EventForwarder event_forwarder_;
+
   // Logs received frames when enabled.
   Http2TraceLogger receive_logger_;
   // Logs sent frames when enabled.