Add a new MetadataVisitor class to QuicSpdyStream.
If registered, an instance of this new class will receive decoded HTTP/3
METADATA frames. If not, registered, HTTP/3 METADATA frames will be delivered
as unknown HTTP/3 frames, which is current behavior.

This is motivated by implements HTTP/3 METADATA handling in Envoy. Without
this new visitor, we need to hook into the EnvoyQuicSession's OnUnknownFrame
methods. But this is really annoying because we receive frames on the Stream,
bounce them (at the QUICHE layer) over the the Session which parses them into
Headers and then need to bounce them back over to the Stream. This is awkward.
By using this new Visitor, the Stream itself can handle METADATA.

Protected by FLAGS_quic_reloadable_flag_quic_enable_http3_metadata_decoding.

PiperOrigin-RevId: 611542297
diff --git a/quiche/quic/core/http/http_decoder.cc b/quiche/quic/core/http/http_decoder.cc
index 94cc367..7a0828c 100644
--- a/quiche/quic/core/http/http_decoder.cc
+++ b/quiche/quic/core/http/http_decoder.cc
@@ -43,7 +43,9 @@
       current_type_field_length_(0),
       remaining_type_field_length_(0),
       error_(QUIC_NO_ERROR),
-      error_detail_("") {
+      error_detail_(""),
+      enable_metadata_decoding_(
+          GetQuicReloadableFlag(quic_enable_http3_metadata_decoding)) {
   QUICHE_DCHECK(visitor_);
 }
 
@@ -282,6 +284,14 @@
       continue_processing = visitor_->OnAcceptChFrameStart(header_length);
       break;
     default:
+      if (enable_metadata_decoding_ &&
+          current_frame_type_ ==
+              static_cast<uint64_t>(HttpFrameType::METADATA)) {
+        QUIC_RELOADABLE_FLAG_COUNT_N(quic_enable_http3_metadata_decoding, 1, 3);
+        continue_processing = visitor_->OnMetadataFrameStart(
+            header_length, current_frame_length_);
+        break;
+      }
       continue_processing = visitor_->OnUnknownFrameStart(
           current_frame_type_, header_length, current_frame_length_);
       break;
@@ -376,6 +386,20 @@
       break;
     }
     default: {
+      if (enable_metadata_decoding_ &&
+          current_frame_type_ ==
+              static_cast<uint64_t>(HttpFrameType::METADATA)) {
+        QUIC_RELOADABLE_FLAG_COUNT_N(quic_enable_http3_metadata_decoding, 2, 3);
+        QuicByteCount bytes_to_read = std::min<QuicByteCount>(
+            remaining_frame_length_, reader->BytesRemaining());
+        absl::string_view payload;
+        bool success = reader->ReadStringPiece(&payload, bytes_to_read);
+        QUICHE_DCHECK(success);
+        QUICHE_DCHECK(!payload.empty());
+        continue_processing = visitor_->OnMetadataFramePayload(payload);
+        remaining_frame_length_ -= payload.length();
+        break;
+      }
       continue_processing = HandleUnknownFramePayload(reader);
       break;
     }
@@ -432,6 +456,12 @@
       break;
     }
     default:
+      if (enable_metadata_decoding_ &&
+          current_frame_type_ ==
+              static_cast<uint64_t>(HttpFrameType::METADATA)) {
+        continue_processing = visitor_->OnMetadataFrameEnd();
+        break;
+      }
       continue_processing = visitor_->OnUnknownFrameEnd();
   }
 
diff --git a/quiche/quic/core/http/http_decoder.h b/quiche/quic/core/http/http_decoder.h
index d8cd6a8..a08c562 100644
--- a/quiche/quic/core/http/http_decoder.h
+++ b/quiche/quic/core/http/http_decoder.h
@@ -97,6 +97,20 @@
     virtual void OnWebTransportStreamFrameType(
         QuicByteCount header_length, WebTransportSessionId session_id) = 0;
 
+    // Called when a METADATA frame has been received.
+    // |header_length| and |payload_length| are the length of the frame header
+    // and payload, respectively.
+    virtual bool OnMetadataFrameStart(QuicByteCount header_length,
+                                      QuicByteCount payload_length) = 0;
+
+    // Called when part of the payload of the METADATA frame has been read.  May
+    // be called multiple times for a single frame.  |payload| is guaranteed to
+    // be non-empty.
+    virtual bool OnMetadataFramePayload(absl::string_view payload) = 0;
+
+    // Called when the METADATA frame has been completely processed.
+    virtual bool OnMetadataFrameEnd() = 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
@@ -268,6 +282,9 @@
   std::array<char, sizeof(uint64_t)> length_buffer_;
   // Remaining unparsed type field data.
   std::array<char, sizeof(uint64_t)> type_buffer_;
+
+  // Latched value of --quic_enable_http3_metadata_decoding.
+  const bool enable_metadata_decoding_;
 };
 
 }  // namespace quic
diff --git a/quiche/quic/core/http/http_decoder_test.cc b/quiche/quic/core/http/http_decoder_test.cc
index 252b637..f0af1c5 100644
--- a/quiche/quic/core/http/http_decoder_test.cc
+++ b/quiche/quic/core/http/http_decoder_test.cc
@@ -56,6 +56,9 @@
     ON_CALL(visitor_, OnPriorityUpdateFrame(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnAcceptChFrameStart(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnAcceptChFrame(_)).WillByDefault(Return(true));
+    ON_CALL(visitor_, OnMetadataFrameStart(_, _)).WillByDefault(Return(true));
+    ON_CALL(visitor_, OnMetadataFramePayload(_)).WillByDefault(Return(true));
+    ON_CALL(visitor_, OnMetadataFrameEnd()).WillByDefault(Return(true));
     ON_CALL(visitor_, OnUnknownFrameStart(_, _, _)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnUnknownFramePayload(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnUnknownFrameEnd()).WillByDefault(Return(true));
@@ -473,6 +476,58 @@
   EXPECT_EQ("", decoder_.error_detail());
 }
 
+TEST_F(HttpDecoderTest, MetadataFrame) {
+  if (!GetQuicReloadableFlag(quic_enable_http3_metadata_decoding)) {
+    return;
+  }
+  InSequence s;
+  std::string input =
+      absl::StrCat(absl::HexStringToBytes("404d"  // 2 byte type (METADATA)
+                                          "08"),  // length
+                   "Metadata");                   // headers
+
+  // Visitor pauses processing.
+  EXPECT_CALL(visitor_, OnMetadataFrameStart(3, 8)).WillOnce(Return(false));
+  absl::string_view remaining_input(input);
+  QuicByteCount processed_bytes =
+      ProcessInputWithGarbageAppended(remaining_input);
+  EXPECT_EQ(3u, processed_bytes);
+  remaining_input = remaining_input.substr(processed_bytes);
+
+  EXPECT_CALL(visitor_, OnMetadataFramePayload(absl::string_view("Metadata")))
+      .WillOnce(Return(false));
+  processed_bytes = ProcessInputWithGarbageAppended(remaining_input);
+  EXPECT_EQ(remaining_input.size(), processed_bytes);
+
+  EXPECT_CALL(visitor_, OnMetadataFrameEnd()).WillOnce(Return(false));
+  EXPECT_EQ(0u, ProcessInputWithGarbageAppended(""));
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  // Process the full frame.
+  EXPECT_CALL(visitor_, OnMetadataFrameStart(3, 8));
+  EXPECT_CALL(visitor_, OnMetadataFramePayload(absl::string_view("Metadata")));
+  EXPECT_CALL(visitor_, OnMetadataFrameEnd());
+  EXPECT_EQ(input.size(), ProcessInput(input));
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  // Process the frame incrementally.
+  EXPECT_CALL(visitor_, OnMetadataFrameStart(3, 8));
+  EXPECT_CALL(visitor_, OnMetadataFramePayload(absl::string_view("M")));
+  EXPECT_CALL(visitor_, OnMetadataFramePayload(absl::string_view("e")));
+  EXPECT_CALL(visitor_, OnMetadataFramePayload(absl::string_view("t")));
+  EXPECT_CALL(visitor_, OnMetadataFramePayload(absl::string_view("a")));
+  EXPECT_CALL(visitor_, OnMetadataFramePayload(absl::string_view("d")));
+  EXPECT_CALL(visitor_, OnMetadataFramePayload(absl::string_view("a")));
+  EXPECT_CALL(visitor_, OnMetadataFramePayload(absl::string_view("t")));
+  EXPECT_CALL(visitor_, OnMetadataFramePayload(absl::string_view("a")));
+  EXPECT_CALL(visitor_, OnMetadataFrameEnd());
+  ProcessInputCharByChar(input);
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+}
+
 TEST_F(HttpDecoderTest, EmptyDataFrame) {
   InSequence s;
   std::string input = absl::HexStringToBytes(
diff --git a/quiche/quic/core/http/quic_receive_control_stream.cc b/quiche/quic/core/http/quic_receive_control_stream.cc
index 6a57d5f..4cba9d5 100644
--- a/quiche/quic/core/http/quic_receive_control_stream.cc
+++ b/quiche/quic/core/http/quic_receive_control_stream.cc
@@ -172,6 +172,22 @@
       << "Parsed WEBTRANSPORT_STREAM on a control stream.";
 }
 
+bool QuicReceiveControlStream::OnMetadataFrameStart(
+    QuicByteCount /*header_length*/, QuicByteCount /*payload_length*/) {
+  return ValidateFrameType(HttpFrameType::METADATA);
+}
+
+bool QuicReceiveControlStream::OnMetadataFramePayload(
+    absl::string_view /*payload*/) {
+  // Ignore METADATA frames.
+  return true;
+}
+
+bool QuicReceiveControlStream::OnMetadataFrameEnd() {
+  // Ignore METADATA frames.
+  return true;
+}
+
 bool QuicReceiveControlStream::OnUnknownFrameStart(
     uint64_t frame_type, QuicByteCount /*header_length*/,
     QuicByteCount payload_length) {
diff --git a/quiche/quic/core/http/quic_receive_control_stream.h b/quiche/quic/core/http/quic_receive_control_stream.h
index e3ad39d..c7f4ee2 100644
--- a/quiche/quic/core/http/quic_receive_control_stream.h
+++ b/quiche/quic/core/http/quic_receive_control_stream.h
@@ -52,6 +52,10 @@
   bool OnAcceptChFrame(const AcceptChFrame& frame) override;
   void OnWebTransportStreamFrameType(QuicByteCount header_length,
                                      WebTransportSessionId session_id) override;
+  bool OnMetadataFrameStart(QuicByteCount header_length,
+                            QuicByteCount payload_length) override;
+  bool OnMetadataFramePayload(absl::string_view payload) override;
+  bool OnMetadataFrameEnd() override;
   bool OnUnknownFrameStart(uint64_t frame_type, QuicByteCount header_length,
                            QuicByteCount payload_length) override;
   bool OnUnknownFramePayload(absl::string_view payload) override;
diff --git a/quiche/quic/core/http/quic_spdy_session.cc b/quiche/quic/core/http/quic_spdy_session.cc
index 48c363a..df57d20 100644
--- a/quiche/quic/core/http/quic_spdy_session.cc
+++ b/quiche/quic/core/http/quic_spdy_session.cc
@@ -147,6 +147,19 @@
       WebTransportSessionId /*session_id*/) override {
     QUICHE_NOTREACHED();
   }
+  bool OnMetadataFrameStart(QuicByteCount /*header_length*/,
+                            QuicByteCount /*payload_length*/) override {
+    error_detail_ = "METADATA frame forbidden";
+    return false;
+  }
+  bool OnMetadataFramePayload(absl::string_view /*payload*/) override {
+    QUICHE_NOTREACHED();
+    return false;
+  }
+  bool OnMetadataFrameEnd() override {
+    QUICHE_NOTREACHED();
+    return false;
+  }
   bool OnUnknownFrameStart(uint64_t /*frame_type*/,
                            QuicByteCount
                            /*header_length*/,
diff --git a/quiche/quic/core/http/quic_spdy_stream.cc b/quiche/quic/core/http/quic_spdy_stream.cc
index 64757bc..77233fd 100644
--- a/quiche/quic/core/http/quic_spdy_stream.cc
+++ b/quiche/quic/core/http/quic_spdy_stream.cc
@@ -144,6 +144,32 @@
     stream_->OnWebTransportStreamFrameType(header_length, session_id);
   }
 
+  bool OnMetadataFrameStart(QuicByteCount header_length,
+                            QuicByteCount payload_length) override {
+    if (!VersionUsesHttp3(stream_->transport_version())) {
+      CloseConnectionOnWrongFrame("Metadata");
+      return false;
+    }
+    return stream_->OnMetadataFrameStart(header_length, payload_length);
+  }
+
+  bool OnMetadataFramePayload(absl::string_view payload) override {
+    QUICHE_DCHECK(!payload.empty());
+    if (!VersionUsesHttp3(stream_->transport_version())) {
+      CloseConnectionOnWrongFrame("Metadata");
+      return false;
+    }
+    return stream_->OnMetadataFramePayload(payload);
+  }
+
+  bool OnMetadataFrameEnd() override {
+    if (!VersionUsesHttp3(stream_->transport_version())) {
+      CloseConnectionOnWrongFrame("Metadata");
+      return false;
+    }
+    return stream_->OnMetadataFrameEnd();
+  }
+
   bool OnUnknownFrameStart(uint64_t frame_type, QuicByteCount header_length,
                            QuicByteCount payload_length) override {
     return stream_->OnUnknownFrameStart(frame_type, header_length,
@@ -697,6 +723,16 @@
   }
 }
 
+void QuicSpdyStream::RegisterMetadataVisitor(MetadataVisitor* visitor) {
+  QUIC_BUG_IF(Metadata visitor requires http3 metadata flag,
+              !GetQuicReloadableFlag(quic_enable_http3_metadata_decoding));
+  metadata_visitor_ = visitor;
+}
+
+void QuicSpdyStream::UnregisterMetadataVisitor() {
+  metadata_visitor_ = nullptr;
+}
+
 void QuicSpdyStream::OnPriorityFrame(
     const spdy::SpdyStreamPrecedence& precedence) {
   QUICHE_DCHECK_EQ(Perspective::IS_SERVER,
@@ -1164,6 +1200,102 @@
                                                                 id());
 }
 
+bool QuicSpdyStream::OnMetadataFrameStart(QuicByteCount header_length,
+                                          QuicByteCount payload_length) {
+  if (metadata_visitor_ == nullptr) {
+    return OnUnknownFrameStart(
+        static_cast<uint64_t>(quic::HttpFrameType::METADATA), header_length,
+        payload_length);
+  }
+  QUIC_BUG_IF(Invalid METADATA state, received_metadata_payload_ != nullptr)
+      << "Invalid metadata state";
+  received_metadata_payload_ =
+      std::make_unique<ReceivedMetadataPayload>(payload_length);
+  received_metadata_payload_->frame_len = header_length + payload_length;
+
+  // Consume the frame header.
+  QUIC_DVLOG(1) << ENDPOINT << "Consuming " << header_length
+                << " byte long frame header of METADATA.";
+  sequencer()->MarkConsumed(body_manager_.OnNonBody(header_length));
+  return true;
+}
+
+bool QuicSpdyStream::OnMetadataFramePayload(absl::string_view payload) {
+  if (metadata_visitor_ == nullptr) {
+    return OnUnknownFramePayload(payload);
+  }
+  received_metadata_payload_->buffer.push_back(std::string(payload));
+  received_metadata_payload_->bytes_remaining -= payload.size();
+
+  // Consume the frame payload.
+  QUIC_DVLOG(1) << ENDPOINT << "Consuming " << payload.size()
+                << " bytes of payload of METADATA.";
+  sequencer()->MarkConsumed(body_manager_.OnNonBody(payload.size()));
+  return true;
+}
+namespace {
+class MetadataHeadersDecoder : public QpackDecodedHeadersAccumulator::Visitor {
+ public:
+  void OnHeadersDecoded(QuicHeaderList headers,
+                        bool header_list_size_limit_exceeded) override {
+    header_list_size_limit_exceeded_ = header_list_size_limit_exceeded;
+    headers_ = std::move(headers);
+  }
+
+  void OnHeaderDecodingError(QuicErrorCode error_code,
+                             absl::string_view error_message) override {
+    error_code_ = error_code;
+    error_message_ = absl::StrCat("Error decoding metadata: ", error_message);
+  }
+
+  QuicErrorCode error_code() { return error_code_; }
+  std::string error_message() { return error_message_; }
+  QuicHeaderList& headers() { return headers_; }
+  bool header_list_size_limit_exceeded() {
+    return header_list_size_limit_exceeded_;
+  }
+
+ private:
+  QuicErrorCode error_code_ = QUIC_NO_ERROR;
+  QuicHeaderList headers_;
+  std::string error_message_;
+  bool header_list_size_limit_exceeded_ = false;
+};
+}  // namespace
+
+bool QuicSpdyStream::OnMetadataFrameEnd() {
+  if (metadata_visitor_ == nullptr) {
+    return OnUnknownFrameEnd();
+  }
+  QUIC_BUG_IF(METADATA bytes remaining,
+              received_metadata_payload_->bytes_remaining != 0)
+      << "More metadata remaining: "
+      << received_metadata_payload_->bytes_remaining;
+
+  quic::NoopEncoderStreamErrorDelegate delegate;
+  quic::QpackDecoder qpack_decoder(/*maximum_dynamic_table_capacity=*/0,
+                                   /*maximum_blocked_streams=*/0, &delegate);
+
+  constexpr size_t kMaxMetadataBlockSize = 1 << 20;  // 1 MB
+  MetadataHeadersDecoder decoder;
+  quic::QpackDecodedHeadersAccumulator accumulator(
+      id(), &qpack_decoder, &decoder, kMaxMetadataBlockSize);
+  for (const std::string& slice : received_metadata_payload_->buffer) {
+    accumulator.Decode(slice);
+    if (decoder.error_code() != QUIC_NO_ERROR) {
+      OnUnrecoverableError(QUIC_DECOMPRESSION_FAILURE, decoder.error_message());
+      return false;
+    }
+  }
+
+  accumulator.EndHeaderBlock();
+
+  metadata_visitor_->OnMetadataComplete(received_metadata_payload_->frame_len,
+                                        decoder.headers());
+  received_metadata_payload_.reset();
+  return true;
+}
+
 bool QuicSpdyStream::OnUnknownFrameStart(uint64_t frame_type,
                                          QuicByteCount header_length,
                                          QuicByteCount payload_length) {
diff --git a/quiche/quic/core/http/quic_spdy_stream.h b/quiche/quic/core/http/quic_spdy_stream.h
index 5835a77..10c34b1 100644
--- a/quiche/quic/core/http/quic_spdy_stream.h
+++ b/quiche/quic/core/http/quic_spdy_stream.h
@@ -71,6 +71,43 @@
     virtual ~Visitor() {}
   };
 
+  // Class which receives HTTP/3 METADATA.
+  class QUICHE_EXPORT MetadataVisitor {
+   public:
+    virtual ~MetadataVisitor() = default;
+
+    // Called when HTTP/3 METADATA has been received and parsed.
+    virtual void OnMetadataComplete(size_t frame_len,
+                                    const QuicHeaderList& header_list) = 0;
+  };
+
+  class QUICHE_EXPORT Http3DatagramVisitor {
+   public:
+    virtual ~Http3DatagramVisitor() {}
+
+    // Called when an HTTP/3 datagram is received. |payload| does not contain
+    // the stream ID.
+    virtual void OnHttp3Datagram(QuicStreamId stream_id,
+                                 absl::string_view payload) = 0;
+
+    // Called when a Capsule with an unknown type is received.
+    virtual void OnUnknownCapsule(QuicStreamId stream_id,
+                                  const quiche::UnknownCapsule& capsule) = 0;
+  };
+
+  class QUICHE_EXPORT ConnectIpVisitor {
+   public:
+    virtual ~ConnectIpVisitor() {}
+
+    virtual bool OnAddressAssignCapsule(
+        const quiche::AddressAssignCapsule& capsule) = 0;
+    virtual bool OnAddressRequestCapsule(
+        const quiche::AddressRequestCapsule& capsule) = 0;
+    virtual bool OnRouteAdvertisementCapsule(
+        const quiche::RouteAdvertisementCapsule& capsule) = 0;
+    virtual void OnHeadersWritten() = 0;
+  };
+
   QuicSpdyStream(QuicStreamId id, QuicSpdySession* spdy_session,
                  StreamType type);
   QuicSpdyStream(PendingStream* pending, QuicSpdySession* spdy_session);
@@ -257,20 +294,6 @@
   // to allow mocking in tests.
   virtual MessageStatus SendHttp3Datagram(absl::string_view payload);
 
-  class QUICHE_EXPORT Http3DatagramVisitor {
-   public:
-    virtual ~Http3DatagramVisitor() {}
-
-    // Called when an HTTP/3 datagram is received. |payload| does not contain
-    // the stream ID.
-    virtual void OnHttp3Datagram(QuicStreamId stream_id,
-                                 absl::string_view payload) = 0;
-
-    // Called when a Capsule with an unknown type is received.
-    virtual void OnUnknownCapsule(QuicStreamId stream_id,
-                                  const quiche::UnknownCapsule& capsule) = 0;
-  };
-
   // Registers |visitor| to receive HTTP/3 datagrams and enables Capsule
   // Protocol by registering a CapsuleParser. |visitor| must be valid until a
   // corresponding call to UnregisterHttp3DatagramVisitor.
@@ -284,19 +307,6 @@
   // Mainly meant to be used by the visitors' move operators.
   void ReplaceHttp3DatagramVisitor(Http3DatagramVisitor* visitor);
 
-  class QUICHE_EXPORT ConnectIpVisitor {
-   public:
-    virtual ~ConnectIpVisitor() {}
-
-    virtual bool OnAddressAssignCapsule(
-        const quiche::AddressAssignCapsule& capsule) = 0;
-    virtual bool OnAddressRequestCapsule(
-        const quiche::AddressRequestCapsule& capsule) = 0;
-    virtual bool OnRouteAdvertisementCapsule(
-        const quiche::RouteAdvertisementCapsule& capsule) = 0;
-    virtual void OnHeadersWritten() = 0;
-  };
-
   // Registers |visitor| to receive CONNECT-IP capsules. |visitor| must be
   // valid until a corresponding call to UnregisterConnectIpVisitor.
   void RegisterConnectIpVisitor(ConnectIpVisitor* visitor);
@@ -325,6 +335,11 @@
     return invalid_request_details_;
   }
 
+  // Registers |visitor| to receive HTTP/3 METADATA. |visitor| must be valid
+  // until a corresponding call to UnregisterRegisterMetadataVisitor.
+  void RegisterMetadataVisitor(MetadataVisitor* visitor);
+  void UnregisterMetadataVisitor();
+
  protected:
   // Called when the received headers are too large. By default this will
   // reset the stream.
@@ -397,6 +412,10 @@
   bool OnHeadersFrameEnd();
   void OnWebTransportStreamFrameType(QuicByteCount header_length,
                                      WebTransportSessionId session_id);
+  bool OnMetadataFrameStart(QuicByteCount header_length,
+                            QuicByteCount payload_length);
+  bool OnMetadataFramePayload(absl::string_view payload);
+  bool OnMetadataFrameEnd();
   bool OnUnknownFrameStart(uint64_t frame_type, QuicByteCount header_length,
                            QuicByteCount payload_length);
   bool OnUnknownFramePayload(absl::string_view payload);
@@ -501,6 +520,19 @@
   // CONNECT-IP support.
   ConnectIpVisitor* connect_ip_visitor_ = nullptr;
 
+  // Present if HTTP/3 METADATA frames should be parsed.
+  MetadataVisitor* metadata_visitor_ = nullptr;
+  struct ReceivedMetadataPayload {
+    explicit ReceivedMetadataPayload(size_t remaining)
+        : bytes_remaining(remaining) {}
+
+    std::list<std::string> buffer;
+    size_t frame_len = 0;
+    size_t bytes_remaining = 0;
+  };
+  // Present if an HTTP/3 METADATA is currently being parsed.
+  std::unique_ptr<ReceivedMetadataPayload> received_metadata_payload_;
+
   // Empty if the headers are valid.
   std::string invalid_request_details_;
 };
diff --git a/quiche/quic/core/http/quic_spdy_stream_test.cc b/quiche/quic/core/http/quic_spdy_stream_test.cc
index 2e08a5c..4f6ca18 100644
--- a/quiche/quic/core/http/quic_spdy_stream_test.cc
+++ b/quiche/quic/core/http/quic_spdy_stream_test.cc
@@ -2728,6 +2728,81 @@
   OnStreamFrame(unknown_frame);
 }
 
+TEST_P(QuicSpdyStreamIncrementalConsumptionTest,
+       ReceiveUnsupportedMetadataFrame) {
+  if (!UsesHttp3()) {
+    return;
+  }
+
+  Initialize(kShouldProcessData);
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session_->set_debug_visitor(&debug_visitor);
+
+  quiche::HttpHeaderBlock headers;
+  headers.AppendValueOrAddHeader("key1", "val1");
+  headers.AppendValueOrAddHeader("key2", "val2");
+  quic::NoopDecoderStreamErrorDelegate delegate;
+  QpackEncoder qpack_encoder(&delegate, quic::HuffmanEncoding::kDisabled);
+  std::string metadata_frame_payload = qpack_encoder.EncodeHeaderList(
+      stream_->id(), headers,
+      /* encoder_stream_sent_byte_count = */ nullptr);
+  std::string metadata_frame_header =
+      quic::HttpEncoder::SerializeMetadataFrameHeader(
+          metadata_frame_payload.size());
+  std::string metadata_frame = metadata_frame_header + metadata_frame_payload;
+
+  EXPECT_CALL(debug_visitor,
+              OnUnknownFrameReceived(
+                  stream_->id(), /* frame_type = */ 0x4d,
+                  /* payload_length = */ metadata_frame_payload.length()));
+  OnStreamFrame(metadata_frame);
+}
+
+class MockMetadataVisitor : public QuicSpdyStream::MetadataVisitor {
+ public:
+  ~MockMetadataVisitor() override = default;
+  MOCK_METHOD(void, OnMetadataComplete,
+              (size_t frame_len, const QuicHeaderList& header_list),
+              (override));
+};
+
+TEST_P(QuicSpdyStreamIncrementalConsumptionTest, ReceiveMetadataFrame) {
+  if (!UsesHttp3() ||
+      !GetQuicReloadableFlag(quic_enable_http3_metadata_decoding)) {
+    return;
+  }
+  StrictMock<MockMetadataVisitor> metadata_visitor;
+  Initialize(kShouldProcessData);
+  stream_->RegisterMetadataVisitor(&metadata_visitor);
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session_->set_debug_visitor(&debug_visitor);
+
+  quiche::HttpHeaderBlock headers;
+  headers.AppendValueOrAddHeader("key1", "val1");
+  headers.AppendValueOrAddHeader("key2", "val2");
+  quic::NoopDecoderStreamErrorDelegate delegate;
+  QpackEncoder qpack_encoder(&delegate, quic::HuffmanEncoding::kDisabled);
+  std::string metadata_frame_payload = qpack_encoder.EncodeHeaderList(
+      stream_->id(), headers,
+      /* encoder_stream_sent_byte_count = */ nullptr);
+  std::string metadata_frame_header =
+      quic::HttpEncoder::SerializeMetadataFrameHeader(
+          metadata_frame_payload.size());
+  std::string metadata_frame = metadata_frame_header + metadata_frame_payload;
+
+  EXPECT_CALL(metadata_visitor, OnMetadataComplete(metadata_frame.size(), _))
+      .WillOnce(testing::WithArgs<1>(
+          Invoke([&headers](const QuicHeaderList& header_list) {
+            quiche::HttpHeaderBlock actual_headers;
+            for (const auto& header : header_list) {
+              actual_headers.AppendValueOrAddHeader(header.first,
+                                                    header.second);
+            }
+            EXPECT_EQ(headers, actual_headers);
+          })));
+  OnStreamFrame(metadata_frame);
+}
+
 TEST_P(QuicSpdyStreamIncrementalConsumptionTest, UnknownFramesInterleaved) {
   if (!UsesHttp3()) {
     return;
diff --git a/quiche/quic/core/quic_flags_list.h b/quiche/quic/core/quic_flags_list.h
index bb7d09d..d2b1864 100644
--- a/quiche/quic/core/quic_flags_list.h
+++ b/quiche/quic/core/quic_flags_list.h
@@ -91,6 +91,8 @@
 QUIC_FLAG(quic_reloadable_flag_quic_disable_batch_write, false)
 // If true, set burst token to 2 in cwnd bootstrapping experiment.
 QUIC_FLAG(quic_reloadable_flag_quic_conservative_bursts, false)
+// If true, the HTTP/3 decoder will decode METADATA frames and not treat them as Unknown.
+QUIC_FLAG(quic_reloadable_flag_quic_enable_http3_metadata_decoding, false)
 // If true, use BBRv2 as the default congestion controller. Takes precedence over --quic_default_to_bbr.
 QUIC_FLAG(quic_reloadable_flag_quic_default_to_bbr_v2, false)
 // If true, use a LRU cache to record client addresses of packets received on server\'s original address.
diff --git a/quiche/quic/test_tools/quic_test_utils.h b/quiche/quic/test_tools/quic_test_utils.h
index 7de2c2a..72f5bd0 100644
--- a/quiche/quic/test_tools/quic_test_utils.h
+++ b/quiche/quic/test_tools/quic_test_utils.h
@@ -1504,6 +1504,12 @@
               (QuicByteCount header_length, WebTransportSessionId session_id),
               (override));
 
+  MOCK_METHOD(bool, OnMetadataFrameStart,
+              (QuicByteCount header_length, QuicByteCount payload_length),
+              (override));
+  MOCK_METHOD(bool, OnMetadataFramePayload, (absl::string_view payload),
+              (override));
+  MOCK_METHOD(bool, OnMetadataFrameEnd, (), (override));
   MOCK_METHOD(bool, OnUnknownFrameStart,
               (uint64_t frame_type, QuicByteCount header_length,
                QuicByteCount payload_length),