Parse ACCEPT_CH frame in HttpDecoder.

Protected by FLAGS_quic_reloadable_flag_quic_parse_accept_ch_frame.

PiperOrigin-RevId: 347054156
Change-Id: If456d341c156b0f761f63558f95d5664d4a0a193
diff --git a/quic/core/http/http_decoder.cc b/quic/core/http/http_decoder.cc
index 590f2fc..dc67302 100644
--- a/quic/core/http/http_decoder.cc
+++ b/quic/core/http/http_decoder.cc
@@ -238,9 +238,20 @@
         QUIC_CODE_COUNT_N(quic_new_priority_update_frame, 2, 2);
         continue_processing =
             visitor_->OnPriorityUpdateFrameStart(header_length);
-        break;
+      } else {
+        continue_processing = visitor_->OnUnknownFrameStart(
+            current_frame_type_, header_length, current_frame_length_);
       }
-      ABSL_FALLTHROUGH_INTENDED;
+      break;
+    case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH):
+      if (GetQuicReloadableFlag(quic_parse_accept_ch_frame)) {
+        QUIC_RELOADABLE_FLAG_COUNT(quic_parse_accept_ch_frame);
+        continue_processing = visitor_->OnAcceptChFrameStart(header_length);
+      } else {
+        continue_processing = visitor_->OnUnknownFrameStart(
+            current_frame_type_, header_length, current_frame_length_);
+      }
+      break;
     default:
       continue_processing = visitor_->OnUnknownFrameStart(
           current_frame_type_, header_length, current_frame_length_);
@@ -374,19 +385,23 @@
         // TODO(bnc): Avoid buffering if the entire frame is present, and
         // instead parse directly out of |reader|.
         BufferFramePayload(reader);
-        break;
+      } else {
+        continue_processing = HandleUnknownFramePayload(reader);
       }
-      ABSL_FALLTHROUGH_INTENDED;
+      break;
+    }
+    case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH): {
+      if (GetQuicReloadableFlag(quic_parse_accept_ch_frame)) {
+        // TODO(bnc): Avoid buffering if the entire frame is present, and
+        // instead parse directly out of |reader|.
+        BufferFramePayload(reader);
+      } else {
+        continue_processing = HandleUnknownFramePayload(reader);
+      }
+      break;
     }
     default: {
-      QuicByteCount bytes_to_read = std::min<QuicByteCount>(
-          remaining_frame_length_, reader->BytesRemaining());
-      absl::string_view payload;
-      bool success = reader->ReadStringPiece(&payload, bytes_to_read);
-      DCHECK(success);
-      DCHECK(!payload.empty());
-      continue_processing = visitor_->OnUnknownFramePayload(payload);
-      remaining_frame_length_ -= payload.length();
+      continue_processing = HandleUnknownFramePayload(reader);
       break;
     }
   }
@@ -492,14 +507,28 @@
           return false;
         }
         continue_processing = visitor_->OnPriorityUpdateFrame(frame);
-        break;
+      } else {
+        continue_processing = visitor_->OnUnknownFrameEnd();
       }
-      ABSL_FALLTHROUGH_INTENDED;
-    }
-    default: {
-      continue_processing = visitor_->OnUnknownFrameEnd();
       break;
     }
+    case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH): {
+      if (GetQuicReloadableFlag(quic_parse_accept_ch_frame)) {
+        // TODO(bnc): Avoid buffering if the entire frame is present, and
+        // instead parse directly out of |reader|.
+        AcceptChFrame frame;
+        QuicDataReader reader(buffer_.data(), current_frame_length_);
+        if (!ParseAcceptChFrame(&reader, &frame)) {
+          return false;
+        }
+        continue_processing = visitor_->OnAcceptChFrame(frame);
+      } else {
+        continue_processing = visitor_->OnUnknownFrameEnd();
+      }
+      break;
+    }
+    default:
+      continue_processing = visitor_->OnUnknownFrameEnd();
   }
 
   current_length_field_length_ = 0;
@@ -508,6 +537,17 @@
   return continue_processing;
 }
 
+bool HttpDecoder::HandleUnknownFramePayload(QuicDataReader* reader) {
+  QuicByteCount bytes_to_read = std::min<QuicByteCount>(
+      remaining_frame_length_, reader->BytesRemaining());
+  absl::string_view payload;
+  bool success = reader->ReadStringPiece(&payload, bytes_to_read);
+  DCHECK(success);
+  DCHECK(!payload.empty());
+  remaining_frame_length_ -= payload.length();
+  return visitor_->OnUnknownFramePayload(payload);
+}
+
 void HttpDecoder::DiscardFramePayload(QuicDataReader* reader) {
   QuicByteCount bytes_to_read = std::min<QuicByteCount>(
       remaining_frame_length_, reader->BytesRemaining());
@@ -647,6 +687,26 @@
   return true;
 }
 
+bool HttpDecoder::ParseAcceptChFrame(QuicDataReader* reader,
+                                     AcceptChFrame* frame) {
+  absl::string_view origin;
+  absl::string_view value;
+  while (!reader->IsDoneReading()) {
+    if (!reader->ReadStringPieceVarInt62(&origin)) {
+      RaiseError(QUIC_HTTP_FRAME_ERROR, "Unable to read ACCEPT_CH origin.");
+      return false;
+    }
+    if (!reader->ReadStringPieceVarInt62(&value)) {
+      RaiseError(QUIC_HTTP_FRAME_ERROR, "Unable to read ACCEPT_CH value.");
+      return false;
+    }
+    // Copy data.
+    frame->entries.push_back({std::string(origin.data(), origin.size()),
+                              std::string(value.data(), value.size())});
+  }
+  return true;
+}
+
 QuicByteCount HttpDecoder::MaxFrameLength(uint64_t frame_type) {
   switch (frame_type) {
     case static_cast<uint64_t>(HttpFrameType::CANCEL_PUSH):
@@ -664,6 +724,9 @@
     case static_cast<uint64_t>(HttpFrameType::PRIORITY_UPDATE_REQUEST_STREAM):
       // This limit is arbitrary.
       return 1024 * 1024;
+    case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH):
+      // This limit is arbitrary.
+      return 1024 * 1024;
     default:
       // Other frames require no data buffering, so it's safe to have no limit.
       return std::numeric_limits<QuicByteCount>::max();
diff --git a/quic/core/http/http_decoder.h b/quic/core/http/http_decoder.h
index 479f444..86f3f58 100644
--- a/quic/core/http/http_decoder.h
+++ b/quic/core/http/http_decoder.h
@@ -102,6 +102,13 @@
     // Called when a PRIORITY_UPDATE frame has been successfully parsed.
     virtual bool OnPriorityUpdateFrame(const PriorityUpdateFrame& frame) = 0;
 
+    // Called when an ACCEPT_CH frame has been received.
+    // |header_length| contains ACCEPT_CH frame length and payload length.
+    virtual bool OnAcceptChFrameStart(QuicByteCount header_length) = 0;
+
+    // Called when an ACCEPT_CH frame has been successfully parsed.
+    virtual bool OnAcceptChFrame(const AcceptChFrame& frame) = 0;
+
     // Called when a frame of unknown type |frame_type| has been received.
     // Frame type might be reserved, Visitor must make sure to ignore.
     // |header_length| and |payload_length| are the length of the frame header
@@ -174,6 +181,11 @@
   // had been parsed completely.  Returns whether processing should continue.
   bool FinishParsing();
 
+  // Read payload of unknown frame from |reader| and call
+  // Visitor::OnUnknownFramePayload().  Returns true decoding should continue,
+  // false if it should be paused.
+  bool HandleUnknownFramePayload(QuicDataReader* reader);
+
   // Discards any remaining frame payload from |reader|.
   void DiscardFramePayload(QuicDataReader* reader);
 
@@ -207,6 +219,9 @@
   bool ParseNewPriorityUpdateFrame(QuicDataReader* reader,
                                    PriorityUpdateFrame* frame);
 
+  // Parses the payload of an ACCEPT_CH frame from |reader| into |frame|.
+  bool ParseAcceptChFrame(QuicDataReader* reader, AcceptChFrame* frame);
+
   // Returns the max frame size of a given |frame_type|.
   QuicByteCount MaxFrameLength(uint64_t frame_type);
 
diff --git a/quic/core/http/http_decoder_test.cc b/quic/core/http/http_decoder_test.cc
index 68f9d7f..68248ea 100644
--- a/quic/core/http/http_decoder_test.cc
+++ b/quic/core/http/http_decoder_test.cc
@@ -21,6 +21,7 @@
 #include "net/third_party/quiche/src/common/platform/api/quiche_text_utils.h"
 
 using ::testing::_;
+using ::testing::AnyNumber;
 using ::testing::Eq;
 using ::testing::InSequence;
 using ::testing::Return;
@@ -104,6 +105,12 @@
               (override));
 
   MOCK_METHOD(bool,
+              OnAcceptChFrameStart,
+              (QuicByteCount header_length),
+              (override));
+  MOCK_METHOD(bool, OnAcceptChFrame, (const AcceptChFrame& frame), (override));
+
+  MOCK_METHOD(bool,
               OnUnknownFrameStart,
               (uint64_t frame_type,
                QuicByteCount header_length,
@@ -138,6 +145,8 @@
     ON_CALL(visitor_, OnPriorityUpdateFrameStart(_))
         .WillByDefault(Return(true));
     ON_CALL(visitor_, OnPriorityUpdateFrame(_)).WillByDefault(Return(true));
+    ON_CALL(visitor_, OnAcceptChFrameStart(_)).WillByDefault(Return(true));
+    ON_CALL(visitor_, OnAcceptChFrame(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnUnknownFrameStart(_, _, _)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnUnknownFramePayload(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnUnknownFrameEnd()).WillByDefault(Return(true));
@@ -1192,6 +1201,153 @@
   }
 }
 
+TEST_F(HttpDecoderTest, AcceptChFrame) {
+  if (!GetQuicReloadableFlag(quic_parse_accept_ch_frame)) {
+    return;
+  }
+
+  InSequence s;
+  std::string input1 = absl::HexStringToBytes(
+      "4089"  // type (ACCEPT_CH)
+      "00");  // length
+
+  AcceptChFrame accept_ch1;
+
+  // Visitor pauses processing.
+  EXPECT_CALL(visitor_, OnAcceptChFrameStart(3)).WillOnce(Return(false));
+  absl::string_view remaining_input(input1);
+  QuicByteCount processed_bytes =
+      ProcessInputWithGarbageAppended(remaining_input);
+  EXPECT_EQ(3u, processed_bytes);
+  remaining_input = remaining_input.substr(processed_bytes);
+
+  EXPECT_CALL(visitor_, OnAcceptChFrame(accept_ch1)).WillOnce(Return(false));
+  processed_bytes = ProcessInputWithGarbageAppended(remaining_input);
+  EXPECT_EQ(remaining_input.size(), processed_bytes);
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  // Process the full frame.
+  EXPECT_CALL(visitor_, OnAcceptChFrameStart(3));
+  EXPECT_CALL(visitor_, OnAcceptChFrame(accept_ch1));
+  EXPECT_EQ(input1.size(), ProcessInput(input1));
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  // Process the frame incrementally.
+  EXPECT_CALL(visitor_, OnAcceptChFrameStart(3));
+  EXPECT_CALL(visitor_, OnAcceptChFrame(accept_ch1));
+  ProcessInputCharByChar(input1);
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  std::string input2 = absl::HexStringToBytes(
+      "4089"      // type (ACCEPT_CH)
+      "08"        // length
+      "03"        // length of origin
+      "666f6f"    // origin "foo"
+      "03"        // length of value
+      "626172");  // value "bar"
+
+  AcceptChFrame accept_ch2;
+  accept_ch2.entries.push_back({"foo", "bar"});
+
+  // Visitor pauses processing.
+  EXPECT_CALL(visitor_, OnAcceptChFrameStart(3)).WillOnce(Return(false));
+  remaining_input = input2;
+  processed_bytes = ProcessInputWithGarbageAppended(remaining_input);
+  EXPECT_EQ(3u, processed_bytes);
+  remaining_input = remaining_input.substr(processed_bytes);
+
+  EXPECT_CALL(visitor_, OnAcceptChFrame(accept_ch2)).WillOnce(Return(false));
+  processed_bytes = ProcessInputWithGarbageAppended(remaining_input);
+  EXPECT_EQ(remaining_input.size(), processed_bytes);
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  // Process the full frame.
+  EXPECT_CALL(visitor_, OnAcceptChFrameStart(3));
+  EXPECT_CALL(visitor_, OnAcceptChFrame(accept_ch2));
+  EXPECT_EQ(input2.size(), ProcessInput(input2));
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  // Process the frame incrementally.
+  EXPECT_CALL(visitor_, OnAcceptChFrameStart(3));
+  EXPECT_CALL(visitor_, OnAcceptChFrame(accept_ch2));
+  ProcessInputCharByChar(input2);
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+}
+
+TEST_F(HttpDecoderTest, CorruptAcceptChFrame) {
+  if (!GetQuicReloadableFlag(quic_parse_accept_ch_frame)) {
+    // TODO(bnc): merge this test into HttpDecoderTest.CorruptFrame when
+    // deprecating flag.
+    return;
+  }
+
+  EXPECT_CALL(visitor_, OnAcceptChFrameStart(_)).Times(AnyNumber());
+
+  struct {
+    const char* const input;
+    const char* const error_message;
+  } kTestData[] = {{"\x40\x89"  // type (ACCEPT_CH)
+                    "\x01"      // length
+                    "\x40",     // first byte of two-byte varint origin length
+                    "Unable to read ACCEPT_CH origin."},
+                   {"\x40\x89"  // type (ACCEPT_CH)
+                    "\x01"      // length
+                    "\x05",     // valid origin length but no origin string
+                    "Unable to read ACCEPT_CH origin."},
+                   {"\x40\x89"  // type (ACCEPT_CH)
+                    "\x04"      // length
+                    "\x05"      // valid origin length
+                    "foo",      // payload ends before origin ends
+                    "Unable to read ACCEPT_CH origin."},
+                   {"\x40\x89"  // type (ACCEPT_CH)
+                    "\x04"      // length
+                    "\x03"      // valid origin length
+                    "foo",      // payload ends at end of origin: no value
+                    "Unable to read ACCEPT_CH value."},
+                   {"\x40\x89"  // type (ACCEPT_CH)
+                    "\x05"      // length
+                    "\x03"      // valid origin length
+                    "foo"       // payload ends at end of origin: no value
+                    "\x40",     // first byte of two-byte varint value length
+                    "Unable to read ACCEPT_CH value."},
+                   {"\x40\x89"  // type (ACCEPT_CH)
+                    "\x08"      // length
+                    "\x03"      // valid origin length
+                    "foo"       // origin
+                    "\x05"      // valid value length
+                    "bar",      // payload ends before value ends
+                    "Unable to read ACCEPT_CH value."}};
+
+  for (const auto& test_data : kTestData) {
+    {
+      HttpDecoder decoder(&visitor_);
+      EXPECT_CALL(visitor_, OnError(&decoder));
+
+      absl::string_view input(test_data.input);
+      decoder.ProcessInput(input.data(), input.size());
+      EXPECT_THAT(decoder.error(), IsError(QUIC_HTTP_FRAME_ERROR));
+      EXPECT_EQ(test_data.error_message, decoder.error_detail());
+    }
+    {
+      HttpDecoder decoder(&visitor_);
+      EXPECT_CALL(visitor_, OnError(&decoder));
+
+      absl::string_view input(test_data.input);
+      for (auto c : input) {
+        decoder.ProcessInput(&c, 1);
+      }
+      EXPECT_THAT(decoder.error(), IsError(QUIC_HTTP_FRAME_ERROR));
+      EXPECT_EQ(test_data.error_message, decoder.error_detail());
+    }
+  }
+}
+
 TEST_F(HttpDecoderTest, DecodeSettings) {
   std::string input = absl::HexStringToBytes(
       "04"    // type (SETTINGS)
diff --git a/quic/core/http/quic_receive_control_stream.cc b/quic/core/http/quic_receive_control_stream.cc
index cc6857f..17c588d 100644
--- a/quic/core/http/quic_receive_control_stream.cc
+++ b/quic/core/http/quic_receive_control_stream.cc
@@ -252,6 +252,34 @@
   return true;
 }
 
+bool QuicReceiveControlStream::OnAcceptChFrameStart(
+    QuicByteCount /* header_length */) {
+  if (!settings_frame_received_) {
+    stream_delegate()->OnStreamError(
+        QUIC_HTTP_MISSING_SETTINGS_FRAME,
+        "ACCEPT_CH frame received before SETTINGS.");
+    return false;
+  }
+
+  if (spdy_session()->perspective() == Perspective::IS_SERVER) {
+    OnWrongFrame("ACCEPT_CH");
+    return false;
+  }
+
+  return true;
+}
+
+bool QuicReceiveControlStream::OnAcceptChFrame(const AcceptChFrame& frame) {
+  DCHECK_EQ(Perspective::IS_CLIENT, spdy_session()->perspective());
+
+  if (spdy_session()->debug_visitor()) {
+    spdy_session()->debug_visitor()->OnAcceptChFrameReceived(frame);
+  }
+
+  spdy_session()->OnAcceptChFrame(frame);
+  return true;
+}
+
 bool QuicReceiveControlStream::OnUnknownFrameStart(
     uint64_t frame_type,
     QuicByteCount /*header_length*/,
diff --git a/quic/core/http/quic_receive_control_stream.h b/quic/core/http/quic_receive_control_stream.h
index 2654bb8..728aa31 100644
--- a/quic/core/http/quic_receive_control_stream.h
+++ b/quic/core/http/quic_receive_control_stream.h
@@ -56,6 +56,8 @@
   bool OnPushPromiseFrameEnd() override;
   bool OnPriorityUpdateFrameStart(QuicByteCount header_length) override;
   bool OnPriorityUpdateFrame(const PriorityUpdateFrame& frame) override;
+  bool OnAcceptChFrameStart(QuicByteCount header_length) override;
+  bool OnAcceptChFrame(const AcceptChFrame& frame) override;
   bool OnUnknownFrameStart(uint64_t frame_type,
                            QuicByteCount header_length,
                            QuicByteCount payload_length) override;
diff --git a/quic/core/http/quic_receive_control_stream_test.cc b/quic/core/http/quic_receive_control_stream_test.cc
index 51948e6..949a0f4 100644
--- a/quic/core/http/quic_receive_control_stream_test.cc
+++ b/quic/core/http/quic_receive_control_stream_test.cc
@@ -396,6 +396,70 @@
                       /* offset = */ 1, cancel_push_frame));
 }
 
+TEST_P(QuicReceiveControlStreamTest, AcceptChFrameBeforeSettings) {
+  std::string accept_ch_frame = absl::HexStringToBytes(
+      "4089"  // type (ACCEPT_CH)
+      "00");  // length
+
+  EXPECT_CALL(*connection_,
+              CloseConnection(QUIC_HTTP_MISSING_SETTINGS_FRAME,
+                              GetQuicReloadableFlag(quic_parse_accept_ch_frame)
+                                  ? "ACCEPT_CH frame received before SETTINGS."
+                                  : "Unknown frame received before SETTINGS.",
+                              _))
+      .WillOnce(
+          Invoke(connection_, &MockQuicConnection::ReallyCloseConnection));
+  EXPECT_CALL(*connection_, SendConnectionClosePacket(_, _));
+  EXPECT_CALL(session_, OnConnectionClosed(_, _));
+
+  receive_control_stream_->OnStreamFrame(
+      QuicStreamFrame(receive_control_stream_->id(), /* fin = */ false,
+                      /* offset = */ 1, accept_ch_frame));
+}
+
+TEST_P(QuicReceiveControlStreamTest, ReceiveAcceptChFrame) {
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session_.set_debug_visitor(&debug_visitor);
+
+  const QuicStreamId id = receive_control_stream_->id();
+  QuicStreamOffset offset = 1;
+
+  // Receive SETTINGS frame.
+  SettingsFrame settings;
+  std::string settings_frame = EncodeSettings(settings);
+  EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(settings));
+  receive_control_stream_->OnStreamFrame(
+      QuicStreamFrame(id, /* fin = */ false, offset, settings_frame));
+  offset += settings_frame.length();
+
+  // Receive ACCEPT_CH frame.
+  std::string accept_ch_frame = absl::HexStringToBytes(
+      "4089"  // type (ACCEPT_CH)
+      "00");  // length
+
+  if (GetQuicReloadableFlag(quic_parse_accept_ch_frame)) {
+    if (perspective() == Perspective::IS_CLIENT) {
+      EXPECT_CALL(debug_visitor, OnAcceptChFrameReceived(_));
+    } else {
+      EXPECT_CALL(
+          *connection_,
+          CloseConnection(QUIC_HTTP_FRAME_UNEXPECTED_ON_CONTROL_STREAM,
+                          "ACCEPT_CH frame received on control stream", _))
+          .WillOnce(
+              Invoke(connection_, &MockQuicConnection::ReallyCloseConnection));
+      EXPECT_CALL(*connection_, SendConnectionClosePacket(_, _));
+      EXPECT_CALL(session_, OnConnectionClosed(_, _));
+    }
+  } else {
+    EXPECT_CALL(debug_visitor,
+                OnUnknownFrameReceived(id, /* frame_type = */ 0x89,
+                                       /* payload_length = */ 0));
+  }
+
+  receive_control_stream_->OnStreamFrame(
+      QuicStreamFrame(id, /* fin = */ false, offset, accept_ch_frame));
+}
+
 TEST_P(QuicReceiveControlStreamTest, UnknownFrameBeforeSettings) {
   std::string unknown_frame = absl::HexStringToBytes(
       "21"        // reserved frame type
diff --git a/quic/core/http/quic_spdy_session.h b/quic/core/http/quic_spdy_session.h
index 4ab4cfd..a90befa 100644
--- a/quic/core/http/quic_spdy_session.h
+++ b/quic/core/http/quic_spdy_session.h
@@ -90,6 +90,7 @@
   virtual void OnMaxPushIdFrameReceived(const MaxPushIdFrame& /*frame*/) {}
   virtual void OnPriorityUpdateFrameReceived(
       const PriorityUpdateFrame& /*frame*/) {}
+  virtual void OnAcceptChFrameReceived(const AcceptChFrame& /*frame*/) {}
 
   // Incoming HTTP/3 frames on request or push streams.
   virtual void OnDataFrameReceived(QuicStreamId /*stream_id*/,
@@ -196,6 +197,10 @@
   // stream.  Returns false and closes connection if |push_id| is invalid.
   bool OnPriorityUpdateForPushStream(QuicStreamId push_id, int urgency);
 
+  // Called when an HTTP/3 ACCEPT_CH frame has been received.
+  // This method will only be called for client sessions.
+  virtual void OnAcceptChFrame(const AcceptChFrame& /*frame*/) {}
+
   // Sends contents of |iov| to h2_deframer_, returns number of bytes processed.
   size_t ProcessHeaderData(const struct iovec& iov);
 
diff --git a/quic/core/http/quic_spdy_session_test.cc b/quic/core/http/quic_spdy_session_test.cc
index 3606949..e4b5dc0 100644
--- a/quic/core/http/quic_spdy_session_test.cc
+++ b/quic/core/http/quic_spdy_session_test.cc
@@ -351,6 +351,8 @@
                       GetEncryptionLevelToSendApplicationData());
   }
 
+  MOCK_METHOD(void, OnAcceptChFrame, (const AcceptChFrame&), (override));
+
   using QuicSession::closed_streams;
   using QuicSession::ShouldKeepConnectionAlive;
   using QuicSpdySession::ProcessPendingStream;
@@ -3260,6 +3262,57 @@
   session_.OnSettingsFrame(frame);
 }
 
+TEST_P(QuicSpdySessionTestClient, ReceiveAcceptChFrame) {
+  if (!VersionUsesHttp3(transport_version())) {
+    return;
+  }
+
+  if (!GetQuicReloadableFlag(quic_parse_accept_ch_frame)) {
+    return;
+  }
+
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session_.set_debug_visitor(&debug_visitor);
+
+  // Create control stream.
+  QuicStreamId receive_control_stream_id =
+      GetNthServerInitiatedUnidirectionalStreamId(transport_version(), 3);
+  char type[] = {kControlStream};
+  absl::string_view stream_type(type, 1);
+  QuicStreamOffset offset = 0;
+  QuicStreamFrame data1(receive_control_stream_id, /* fin = */ false, offset,
+                        stream_type);
+  offset += stream_type.length();
+  EXPECT_CALL(debug_visitor,
+              OnPeerControlStreamCreated(receive_control_stream_id));
+
+  session_.OnStreamFrame(data1);
+  EXPECT_EQ(receive_control_stream_id,
+            QuicSpdySessionPeer::GetReceiveControlStream(&session_)->id());
+
+  // First frame has to be SETTINGS.
+  std::string serialized_settings = EncodeSettings({});
+  QuicStreamFrame data2(receive_control_stream_id, /* fin = */ false, offset,
+                        serialized_settings);
+  offset += serialized_settings.length();
+  EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(_));
+
+  session_.OnStreamFrame(data2);
+
+  // Receive ACCEPT_CH frame.
+  AcceptChFrame accept_ch;
+  accept_ch.entries.push_back({"foo", "bar"});
+  std::unique_ptr<char[]> buffer;
+  auto frame_length = HttpEncoder::SerializeAcceptChFrame(accept_ch, &buffer);
+  QuicStreamFrame data3(receive_control_stream_id, /* fin = */ false, offset,
+                        absl::string_view(buffer.get(), frame_length));
+
+  EXPECT_CALL(debug_visitor, OnAcceptChFrameReceived(accept_ch));
+  EXPECT_CALL(session_, OnAcceptChFrame(accept_ch));
+
+  session_.OnStreamFrame(data3);
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quic/core/http/quic_spdy_stream.cc b/quic/core/http/quic_spdy_stream.cc
index 62a81ae..8489954 100644
--- a/quic/core/http/quic_spdy_stream.cc
+++ b/quic/core/http/quic_spdy_stream.cc
@@ -154,6 +154,16 @@
     return false;
   }
 
+  bool OnAcceptChFrameStart(QuicByteCount /*header_length*/) override {
+    CloseConnectionOnWrongFrame("ACCEPT_CH");
+    return false;
+  }
+
+  bool OnAcceptChFrame(const AcceptChFrame& /*frame*/) override {
+    CloseConnectionOnWrongFrame("ACCEPT_CH");
+    return false;
+  }
+
   bool OnUnknownFrameStart(uint64_t frame_type,
                            QuicByteCount header_length,
                            QuicByteCount payload_length) override {
diff --git a/quic/core/quic_flags_list.h b/quic/core/quic_flags_list.h
index 28082aa..def3a5c 100644
--- a/quic/core/quic_flags_list.h
+++ b/quic/core/quic_flags_list.h
@@ -44,6 +44,7 @@
 QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_goaway_with_max_stream_id, false)
 QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_granular_qpack_error_codes, true)
 QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_new_priority_update_frame, true)
+QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_parse_accept_ch_frame, true)
 QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_pass_path_response_to_validator, false)
 QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_require_handshake_confirmation, false)
 QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_round_up_tiny_bandwidth, true)
diff --git a/quic/test_tools/quic_test_utils.h b/quic/test_tools/quic_test_utils.h
index 6fbcc17..50afd2d 100644
--- a/quic/test_tools/quic_test_utils.h
+++ b/quic/test_tools/quic_test_utils.h
@@ -1110,6 +1110,10 @@
               OnPriorityUpdateFrameReceived,
               (const PriorityUpdateFrame&),
               (override));
+  MOCK_METHOD(void,
+              OnAcceptChFrameReceived,
+              (const AcceptChFrame&),
+              (override));
 
   MOCK_METHOD(void,
               OnDataFrameReceived,