Update to MoQT draft-10. Use new extensions encoding (just passing a blob)

Status datagrams also have extensions.

PiperOrigin-RevId: 736218941
diff --git a/quiche/quic/moqt/moqt_framer.cc b/quiche/quic/moqt/moqt_framer.cc
index 60ba2ea..5d98ec3 100644
--- a/quiche/quic/moqt/moqt_framer.cc
+++ b/quiche/quic/moqt/moqt_framer.cc
@@ -173,67 +173,6 @@
   const MoqtSubscribeParameters& list_;
 };
 
-class WireExtensionHeader {
- public:
-  explicit WireExtensionHeader(const MoqtExtensionHeader& header)
-      : header_(header) {}
-
-  size_t GetLengthOnWire() {
-    if ((header_.type % 2) == 0) {
-      return quiche::ComputeLengthOnWire(
-          WireVarInt62(header_.type),
-          WireVarInt62(absl::get<uint64_t>(header_.value)));
-    } else {
-      return quiche::ComputeLengthOnWire(
-          WireVarInt62(header_.type),
-          WireStringWithVarInt62Length(absl::get<std::string>(header_.value)));
-    }
-  }
-
-  absl::Status SerializeIntoWriter(quiche::QuicheDataWriter& writer) {
-    if ((header_.type % 2) == 0) {
-      if (!absl::holds_alternative<uint64_t>(header_.value)) {
-        return absl::InvalidArgumentError("Extension type is not uint64_t");
-      }
-      return quiche::SerializeIntoWriter(
-          writer, WireVarInt62(header_.type),
-          WireVarInt62(absl::get<uint64_t>(header_.value)));
-    } else {
-      if (!absl::holds_alternative<std::string>(header_.value)) {
-        return absl::InvalidArgumentError("Extension type is not std::string");
-      }
-      return quiche::SerializeIntoWriter(
-          writer, WireVarInt62(header_.type),
-          WireStringWithVarInt62Length(absl::get<std::string>(header_.value)));
-    }
-  }
-
- private:
-  const MoqtExtensionHeader& header_;
-};
-
-class WireExtensionHeaderList {
- public:
-  explicit WireExtensionHeaderList(
-      const std::vector<MoqtExtensionHeader>& headers)
-      : headers_(headers) {}
-
-  size_t GetLengthOnWire() {
-    return quiche::ComputeLengthOnWire(
-        WireVarInt62(headers_.size()),
-        WireSpan<WireExtensionHeader, MoqtExtensionHeader>(headers_));
-  }
-
-  absl::Status SerializeIntoWriter(quiche::QuicheDataWriter& writer) {
-    return quiche::SerializeIntoWriter(
-        writer, WireVarInt62(headers_.size()),
-        WireSpan<WireExtensionHeader, MoqtExtensionHeader>(headers_));
-  }
-
- private:
-  const std::vector<MoqtExtensionHeader>& headers_;
-};
-
 class WireFullTrackName {
  public:
   using DataType = FullTrackName;
@@ -342,34 +281,34 @@
     switch (message_type) {
       case MoqtDataStreamType::kStreamHeaderSubgroup:
         return (message.payload_length == 0)
-                   ? Serialize(
-                         WireVarInt62(message.object_id),
-                         WireExtensionHeaderList(message.extension_headers),
-                         WireVarInt62(message.payload_length),
-                         WireVarInt62(
-                             static_cast<uint64_t>(message.object_status)))
-                   : Serialize(
-                         WireVarInt62(message.object_id),
-                         WireExtensionHeaderList(message.extension_headers),
-                         WireVarInt62(message.payload_length));
+                   ? Serialize(WireVarInt62(message.object_id),
+                               WireStringWithVarInt62Length(
+                                   message.extension_headers),
+                               WireVarInt62(message.payload_length),
+                               WireVarInt62(static_cast<uint64_t>(
+                                   message.object_status)))
+                   : Serialize(WireVarInt62(message.object_id),
+                               WireStringWithVarInt62Length(
+                                   message.extension_headers),
+                               WireVarInt62(message.payload_length));
       case MoqtDataStreamType::kStreamHeaderFetch:
         return (message.payload_length == 0)
-                   ? Serialize(
-                         WireVarInt62(message.group_id),
-                         WireVarInt62(*message.subgroup_id),
-                         WireVarInt62(message.object_id),
-                         WireUint8(message.publisher_priority),
-                         WireExtensionHeaderList(message.extension_headers),
-                         WireVarInt62(message.payload_length),
-                         WireVarInt62(
-                             static_cast<uint64_t>(message.object_status)))
-                   : Serialize(
-                         WireVarInt62(message.group_id),
-                         WireVarInt62(*message.subgroup_id),
-                         WireVarInt62(message.object_id),
-                         WireUint8(message.publisher_priority),
-                         WireExtensionHeaderList(message.extension_headers),
-                         WireVarInt62(message.payload_length));
+                   ? Serialize(WireVarInt62(message.group_id),
+                               WireVarInt62(*message.subgroup_id),
+                               WireVarInt62(message.object_id),
+                               WireUint8(message.publisher_priority),
+                               WireStringWithVarInt62Length(
+                                   message.extension_headers),
+                               WireVarInt62(message.payload_length),
+                               WireVarInt62(static_cast<uint64_t>(
+                                   message.object_status)))
+                   : Serialize(WireVarInt62(message.group_id),
+                               WireVarInt62(*message.subgroup_id),
+                               WireVarInt62(message.object_id),
+                               WireUint8(message.publisher_priority),
+                               WireStringWithVarInt62Length(
+                                   message.extension_headers),
+                               WireVarInt62(message.payload_length));
       default:
         QUICHE_NOTREACHED();
         return quiche::QuicheBuffer();
@@ -378,42 +317,46 @@
   switch (message_type) {
     case MoqtDataStreamType::kStreamHeaderSubgroup:
       return (message.payload_length == 0)
-                 ? Serialize(WireVarInt62(message_type),
-                             WireVarInt62(message.track_alias),
-                             WireVarInt62(message.group_id),
-                             WireVarInt62(*message.subgroup_id),
-                             WireUint8(message.publisher_priority),
-                             WireVarInt62(message.object_id),
-                             WireExtensionHeaderList(message.extension_headers),
-                             WireVarInt62(message.payload_length),
-                             WireVarInt62(message.object_status))
-                 : Serialize(WireVarInt62(message_type),
-                             WireVarInt62(message.track_alias),
-                             WireVarInt62(message.group_id),
-                             WireVarInt62(*message.subgroup_id),
-                             WireUint8(message.publisher_priority),
-                             WireVarInt62(message.object_id),
-                             WireExtensionHeaderList(message.extension_headers),
-                             WireVarInt62(message.payload_length));
+                 ? Serialize(
+                       WireVarInt62(message_type),
+                       WireVarInt62(message.track_alias),
+                       WireVarInt62(message.group_id),
+                       WireVarInt62(*message.subgroup_id),
+                       WireUint8(message.publisher_priority),
+                       WireVarInt62(message.object_id),
+                       WireStringWithVarInt62Length(message.extension_headers),
+                       WireVarInt62(message.payload_length),
+                       WireVarInt62(message.object_status))
+                 : Serialize(
+                       WireVarInt62(message_type),
+                       WireVarInt62(message.track_alias),
+                       WireVarInt62(message.group_id),
+                       WireVarInt62(*message.subgroup_id),
+                       WireUint8(message.publisher_priority),
+                       WireVarInt62(message.object_id),
+                       WireStringWithVarInt62Length(message.extension_headers),
+                       WireVarInt62(message.payload_length));
     case MoqtDataStreamType::kStreamHeaderFetch:
       return (message.payload_length == 0)
-                 ? Serialize(WireVarInt62(message_type),
-                             WireVarInt62(message.track_alias),
-                             WireVarInt62(message.group_id),
-                             WireVarInt62(*message.subgroup_id),
-                             WireVarInt62(message.object_id),
-                             WireUint8(message.publisher_priority),
-                             WireExtensionHeaderList(message.extension_headers),
-                             WireVarInt62(message.payload_length),
-                             WireVarInt62(message.object_status))
-                 : Serialize(WireVarInt62(message_type),
-                             WireVarInt62(message.track_alias),
-                             WireVarInt62(message.group_id),
-                             WireVarInt62(*message.subgroup_id),
-                             WireVarInt62(message.object_id),
-                             WireUint8(message.publisher_priority),
-                             WireExtensionHeaderList(message.extension_headers),
-                             WireVarInt62(message.payload_length));
+                 ? Serialize(
+                       WireVarInt62(message_type),
+                       WireVarInt62(message.track_alias),
+                       WireVarInt62(message.group_id),
+                       WireVarInt62(*message.subgroup_id),
+                       WireVarInt62(message.object_id),
+                       WireUint8(message.publisher_priority),
+                       WireStringWithVarInt62Length(message.extension_headers),
+                       WireVarInt62(message.payload_length),
+                       WireVarInt62(message.object_status))
+                 : Serialize(
+                       WireVarInt62(message_type),
+                       WireVarInt62(message.track_alias),
+                       WireVarInt62(message.group_id),
+                       WireVarInt62(*message.subgroup_id),
+                       WireVarInt62(message.object_id),
+                       WireUint8(message.publisher_priority),
+                       WireStringWithVarInt62Length(message.extension_headers),
+                       WireVarInt62(message.payload_length));
     default:
       QUICHE_NOTREACHED();
       return quiche::QuicheBuffer();
@@ -437,13 +380,14 @@
         WireVarInt62(MoqtDatagramType::kObjectStatus),
         WireVarInt62(message.track_alias), WireVarInt62(message.group_id),
         WireVarInt62(message.object_id), WireUint8(message.publisher_priority),
+        WireStringWithVarInt62Length(message.extension_headers),
         WireVarInt62(message.object_status));
   }
   return Serialize(
       WireVarInt62(MoqtDatagramType::kObject),
       WireVarInt62(message.track_alias), WireVarInt62(message.group_id),
       WireVarInt62(message.object_id), WireUint8(message.publisher_priority),
-      WireExtensionHeaderList(message.extension_headers),
+      WireStringWithVarInt62Length(message.extension_headers),
       WireVarInt62(message.payload_length), WireBytes(payload));
 }
 
diff --git a/quiche/quic/moqt/moqt_framer_test.cc b/quiche/quic/moqt/moqt_framer_test.cc
index 798e2f0..7a347f5 100644
--- a/quiche/quic/moqt/moqt_framer_test.cc
+++ b/quiche/quic/moqt/moqt_framer_test.cc
@@ -13,7 +13,6 @@
 
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
-#include "absl/types/variant.h"
 #include "quiche/quic/moqt/moqt_messages.h"
 #include "quiche/quic/moqt/moqt_priority.h"
 #include "quiche/quic/moqt/test_tools/moqt_test_message.h"
@@ -301,7 +300,7 @@
       /*group_id=*/5,
       /*object_id=*/6,
       /*publisher_priority=*/7,
-      /*extension_headers=*/std::vector<MoqtExtensionHeader>({}),
+      std::string(kDefaultExtensionBlob.data(), kDefaultExtensionBlob.size()),
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/8,
       /*payload_length=*/3,
@@ -340,7 +339,7 @@
       /*group_id=*/5,
       /*object_id=*/6,
       /*publisher_priority=*/7,
-      /*extension_headers=*/std::vector<MoqtExtensionHeader>({}),
+      std::string(kDefaultExtensionBlob),
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/std::nullopt,
       /*payload_length=*/3,
@@ -371,8 +370,7 @@
       /*group_id=*/5,
       /*object_id=*/6,
       /*publisher_priority=*/7,
-      std::vector<MoqtExtensionHeader>(
-          {MoqtExtensionHeader(0, 12ULL), MoqtExtensionHeader(1, "foo")}),
+      std::string(kDefaultExtensionBlob),
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/std::nullopt,
       /*payload_length=*/3,
@@ -391,7 +389,7 @@
       /*group_id=*/5,
       /*object_id=*/6,
       /*publisher_priority=*/7,
-      std::vector<MoqtExtensionHeader>({}),
+      std::string(kDefaultExtensionBlob),
       /*object_status=*/MoqtObjectStatus::kEndOfGroup,
       /*subgroup_id=*/std::nullopt,
       /*payload_length=*/0,
@@ -533,38 +531,6 @@
   EXPECT_EQ(*end_group, 5);
 }
 
-TEST_F(MoqtFramerSimpleTest, InvalidExtensionType) {
-  MoqtObject object = {
-      /*track_alias=*/4,
-      /*group_id=*/5,
-      /*object_id=*/6,
-      /*publisher_priority=*/7,
-      /*extension_headers=*/
-      std::vector<MoqtExtensionHeader>({MoqtExtensionHeader(0, 12ULL)}),
-      /*object_status=*/MoqtObjectStatus::kNormal,
-      /*subgroup_id=*/std::nullopt,
-      /*payload_length=*/3,
-  };
-  EXPECT_QUIC_BUG(framer_.SerializeObjectHeader(
-                      object, MoqtDataStreamType::kStreamHeaderSubgroup, false),
-                  "Object metadata is invalid");
-
-  object = {
-      /*track_alias=*/4,
-      /*group_id=*/5,
-      /*object_id=*/6,
-      /*publisher_priority=*/7,
-      /*extension_headers=*/
-      std::vector<MoqtExtensionHeader>({MoqtExtensionHeader(1, "foo")}),
-      /*object_status=*/MoqtObjectStatus::kNormal,
-      /*subgroup_id=*/std::nullopt,
-      /*payload_length=*/3,
-  };
-  EXPECT_QUIC_BUG(framer_.SerializeObjectHeader(
-                      object, MoqtDataStreamType::kStreamHeaderSubgroup, false),
-                  "Object metadata is invalid");
-}
-
 TEST_F(MoqtFramerSimpleTest, JoiningFetch) {
   JoiningFetchMessage message;
   quiche::QuicheBuffer buffer =
diff --git a/quiche/quic/moqt/moqt_messages.h b/quiche/quic/moqt/moqt_messages.h
index af78e4d..d961905 100644
--- a/quiche/quic/moqt/moqt_messages.h
+++ b/quiche/quic/moqt/moqt_messages.h
@@ -36,11 +36,11 @@
 }
 
 enum class MoqtVersion : uint64_t {
-  kDraft08 = 0xff000008,
+  kDraft10 = 0xff00000a,
   kUnrecognizedVersionForTests = 0xfe0000ff,
 };
 
-inline constexpr MoqtVersion kDefaultMoqtVersion = MoqtVersion::kDraft08;
+inline constexpr MoqtVersion kDefaultMoqtVersion = MoqtVersion::kDraft10;
 inline constexpr uint64_t kDefaultInitialMaxSubscribeId = 100;
 inline constexpr uint64_t kMinNamespaceElements = 1;
 inline constexpr uint64_t kMaxNamespaceElements = 32;
@@ -340,11 +340,6 @@
 
 MoqtObjectStatus IntegerToObjectStatus(uint64_t integer);
 
-struct MoqtExtensionHeader {
-  uint64_t type;
-  absl::variant<uint64_t, std::string> value;
-};
-
 // The data contained in every Object message, although the message type
 // implies some of the values.
 struct QUICHE_EXPORT MoqtObject {
@@ -352,7 +347,7 @@
   uint64_t group_id;
   uint64_t object_id;
   MoqtPriority publisher_priority;
-  std::vector<MoqtExtensionHeader> extension_headers;
+  std::string extension_headers;  // Raw, unparsed extension headers.
   MoqtObjectStatus object_status;
   std::optional<uint64_t> subgroup_id;
   uint64_t payload_length;
diff --git a/quiche/quic/moqt/moqt_parser.cc b/quiche/quic/moqt/moqt_parser.cc
index 5233a6b..6a79a96 100644
--- a/quiche/quic/moqt/moqt_parser.cc
+++ b/quiche/quic/moqt/moqt_parser.cc
@@ -66,8 +66,6 @@
   return false;
 }
 
-bool IsExtensionTypeVarint(uint64_t type) { return (type % 2 == 0); }
-
 }  // namespace
 
 // The buffering philosophy is complicated, to minimize copying. Here is an
@@ -964,14 +962,17 @@
 std::optional<absl::string_view> ParseDatagram(absl::string_view data,
                                                MoqtObject& object_metadata) {
   uint64_t type_raw, object_status_raw;
+  absl::string_view extensions;
   quic::QuicDataReader reader(data);
   if (!reader.ReadVarInt62(&type_raw) ||
       !reader.ReadVarInt62(&object_metadata.track_alias) ||
       !reader.ReadVarInt62(&object_metadata.group_id) ||
       !reader.ReadVarInt62(&object_metadata.object_id) ||
-      !reader.ReadUInt8(&object_metadata.publisher_priority)) {
+      !reader.ReadUInt8(&object_metadata.publisher_priority) ||
+      !reader.ReadStringPieceVarInt62(&extensions)) {
     return std::nullopt;
   }
+  object_metadata.extension_headers = std::string(extensions);
   if (static_cast<MoqtDatagramType>(type_raw) ==
       MoqtDatagramType::kObjectStatus) {
     object_metadata.payload_length = 0;
@@ -981,30 +982,7 @@
     object_metadata.object_status = IntegerToObjectStatus(object_status_raw);
     return "";
   }
-  uint64_t extensions_remaining;
-  if (!reader.ReadVarInt62(&extensions_remaining)) {
-    return std::nullopt;
-  }
-  while (extensions_remaining > 0) {
-    uint64_t type;
-    if (!reader.ReadVarInt62(&type)) {
-      return std::nullopt;
-    }
-    --extensions_remaining;
-    if (IsExtensionTypeVarint(type)) {
-      uint64_t value;
-      if (!reader.ReadVarInt62(&value)) {
-        return std::nullopt;
-      }
-      object_metadata.extension_headers.push_back({type, value});
-    } else {
-      absl::string_view value;
-      if (!reader.ReadStringPieceVarInt62(&value)) {
-        return std::nullopt;
-      }
-      object_metadata.extension_headers.push_back({type, std::string(value)});
-    }
-  }
+
   absl::string_view payload;
   if (!reader.ReadStringPieceVarInt62(&payload)) {
     return std::nullopt;
@@ -1111,26 +1089,20 @@
       next_input_ = is_fetch ? kObjectId : kPublisherPriority;
       break;
     case kPublisherPriority:
-      next_input_ = is_fetch ? kExtensionCount : kObjectId;
+      next_input_ = is_fetch ? kExtensionSize : kObjectId;
       break;
     case kObjectId:
-      next_input_ = is_fetch ? kPublisherPriority : kExtensionCount;
+      next_input_ = is_fetch ? kPublisherPriority : kExtensionSize;
       break;
-    case kExtensionLength:
-      next_input_ = kExtensionPayload;
+    case kExtensionBody:
+      next_input_ = kObjectPayloadLength;
       break;
-
     case kStatus:
     case kData:
       next_input_ = is_fetch ? kGroupId : kObjectId;
       break;
 
-    case kExtensionCount:       // Either kExtensionType or
-                                // kObjectPayloadLength.
-    case kExtensionType:        // Either kExtensionVarint or kExtensionLength.
-    case kExtensionVarint:      // Either kExtensionType or
-                                // kObjectPayloadLength.
-    case kExtensionPayload:     // Either kExtensionType or
+    case kExtensionSize:        // Either kExtensionBody or
                                 // kObjectPayloadLength.
     case kObjectPayloadLength:  // Either kStatus or kData depending on length.
     case kPadding:              // Handled separately.
@@ -1211,50 +1183,12 @@
       return;
     }
 
-    case kExtensionCount: {
+    case kExtensionSize: {
       std::optional<uint64_t> value_read = ReadVarInt62NoFin();
       if (value_read.has_value()) {
-        extensions_remaining_ = *value_read;
-        next_input_ = (extensions_remaining_ == 0) ? kObjectPayloadLength
-                                                   : kExtensionType;
         metadata_.extension_headers.clear();
-      }
-      return;
-    }
-
-    case kExtensionType: {
-      std::optional<uint64_t> value_read = ReadVarInt62NoFin();
-      if (value_read.has_value()) {
-        --extensions_remaining_;
-        current_extension_.type = *value_read;
-        next_input_ = IsExtensionTypeVarint(*value_read) ? kExtensionVarint
-                                                         : kExtensionLength;
-      }
-      return;
-    }
-
-    case kExtensionVarint: {
-      std::optional<uint64_t> value_read = ReadVarInt62NoFin();
-      if (value_read.has_value()) {
-        next_input_ = (extensions_remaining_ == 0) ? kObjectPayloadLength
-                                                   : kExtensionType;
-        current_extension_.value = *value_read;
-        metadata_.extension_headers.push_back(current_extension_);
-        current_extension_.value = "";
-      }
-      return;
-    }
-
-    case kExtensionLength: {
-      std::optional<uint64_t> value_read = ReadVarInt62NoFin();
-      if (value_read.has_value()) {
-        if (*value_read > 0) {
-          next_input_ = kExtensionPayload;
-          payload_length_remaining_ = *value_read;
-        } else {
-          next_input_ = (extensions_remaining_ == 0) ? kObjectPayloadLength
-                                                     : kExtensionType;
-        }
+        payload_length_remaining_ = *value_read;
+        next_input_ = (value_read == 0) ? kObjectPayloadLength : kExtensionBody;
       }
       return;
     }
@@ -1295,7 +1229,7 @@
       return;
     }
 
-    case kExtensionPayload:
+    case kExtensionBody:
     case kData: {
       while (payload_length_remaining_ > 0) {
         quiche::ReadStream::PeekResult peek_result =
@@ -1322,17 +1256,14 @@
             AdvanceParserState();
           }
         } else {
-          std::get<std::string>(current_extension_.value)
-              .append(peek_result.peeked_data.substr(0, chunk_size));
+          absl::StrAppend(&metadata_.extension_headers,
+                          peek_result.peeked_data.substr(0, chunk_size));
           if (stream_.SkipBytes(chunk_size)) {
             ParseError("FIN received at an unexpected point in the stream");
             return;
           }
           if (done) {
-            next_input_ = (extensions_remaining_ == 0) ? kObjectPayloadLength
-                                                       : kExtensionType;
-            metadata_.extension_headers.push_back(current_extension_);
-            current_extension_.value = "";
+            AdvanceParserState();
           }
         }
       }
diff --git a/quiche/quic/moqt/moqt_parser.h b/quiche/quic/moqt/moqt_parser.h
index 97727d5..d9e5d0f 100644
--- a/quiche/quic/moqt/moqt_parser.h
+++ b/quiche/quic/moqt/moqt_parser.h
@@ -223,11 +223,8 @@
     kSubgroupId,
     kPublisherPriority,
     kObjectId,
-    kExtensionCount,
-    kExtensionType,
-    kExtensionVarint,
-    kExtensionLength,
-    kExtensionPayload,
+    kExtensionSize,
+    kExtensionBody,
     kObjectPayloadLength,
     kStatus,
     kData,
@@ -241,13 +238,10 @@
   struct State {
     NextInput next_input;
     uint64_t payload_remaining;
-    uint64_t extensions_remaining;
 
     bool operator==(const State&) const = default;
   };
-  State state() const {
-    return State{next_input_, payload_length_remaining_, extensions_remaining_};
-  }
+  State state() const { return State{next_input_, payload_length_remaining_}; }
 
   void ReadDataUntil(StopCondition stop_condition);
 
@@ -282,9 +276,7 @@
   NextInput next_input_ = kStreamType;
   MoqtObject metadata_;
   size_t payload_length_remaining_ = 0;
-  uint64_t extensions_remaining_ = 0;
   size_t num_objects_read_ = 0;
-  MoqtExtensionHeader current_extension_;
 
   bool processing_ = false;  // True if currently in ProcessData(), to prevent
                              // re-entrancy.
diff --git a/quiche/quic/moqt/moqt_parser_test.cc b/quiche/quic/moqt/moqt_parser_test.cc
index 03b9856..926980e 100644
--- a/quiche/quic/moqt/moqt_parser_test.cc
+++ b/quiche/quic/moqt/moqt_parser_test.cc
@@ -504,6 +504,27 @@
   EXPECT_FALSE(visitor_.parsing_error_.has_value());
 }
 
+TEST_F(MoqtMessageSpecificTest, ObjectSplitInExtension) {
+  webtransport::test::InMemoryStream stream(/*stream_id=*/0);
+  MoqtDataParser parser(&stream, &visitor_);
+  auto message = std::make_unique<StreamHeaderSubgroupMessage>();
+
+  // first part
+  stream.Receive(message->PacketSample().substr(0, 10), false);
+  parser.ReadAllData();
+  EXPECT_EQ(visitor_.messages_received_, 0);
+
+  // second part
+  stream.Receive(
+      message->PacketSample().substr(10, sizeof(message->total_message_size())),
+      false);
+  parser.ReadAllData();
+  EXPECT_EQ(visitor_.messages_received_, 1);
+  EXPECT_TRUE(visitor_.last_message_.has_value() &&
+              message->EqualFieldValues(*visitor_.last_message_));
+  EXPECT_TRUE(visitor_.end_of_message_);
+}
+
 TEST_F(MoqtMessageSpecificTest, StreamHeaderSubgroupFollowOn) {
   webtransport::test::InMemoryStream stream(/*stream_id=*/0);
   MoqtDataParser parser(&stream, &visitor_);
diff --git a/quiche/quic/moqt/moqt_session_test.cc b/quiche/quic/moqt/moqt_session_test.cc
index c4e9332..a89565d 100644
--- a/quiche/quic/moqt/moqt_session_test.cc
+++ b/quiche/quic/moqt/moqt_session_test.cc
@@ -941,7 +941,7 @@
       /*group_sequence=*/0,
       /*object_sequence=*/0,
       /*publisher_priority=*/0,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/0,
       /*payload_length=*/8,
@@ -967,7 +967,7 @@
       /*group_sequence=*/0,
       /*object_sequence=*/0,
       /*publisher_priority=*/0,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/0,
       /*payload_length=*/16,
@@ -999,7 +999,7 @@
       /*group_sequence=*/0,
       /*object_sequence=*/0,
       /*publisher_priority=*/0,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/0,
       /*payload_length=*/16,
@@ -1026,7 +1026,7 @@
       /*group_sequence=*/0,
       /*object_sequence=*/0,
       /*publisher_priority=*/0,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/0,
       /*payload_length=*/8,
@@ -1072,7 +1072,7 @@
       /*group_sequence=*/0,
       /*object_sequence=*/0,
       /*publisher_priority=*/0,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/0,
       /*payload_length=*/8,
@@ -1440,7 +1440,7 @@
 
   // Reopen the window.
   correct_message = false;
-  // object id, payload length, status.
+  // object id, extensions, payload length, status.
   const std::string kExpectedMessage2 = {0x01, 0x00, 0x00, 0x03};
   EXPECT_CALL(mock_stream, CanWrite()).WillRepeatedly([&] { return true; });
   EXPECT_CALL(*track, GetCachedObject(FullSequence(5, 1))).WillRepeatedly([&] {
@@ -1779,7 +1779,7 @@
       /*group_sequence=*/0,
       /*object_sequence=*/0,
       /*publisher_priority=*/0,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/std::nullopt,
       /*payload_length=*/8,
@@ -1804,7 +1804,7 @@
       /*group_sequence=*/0,
       /*object_sequence=*/0,
       /*publisher_priority=*/0,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/0,
       /*payload_length=*/8,
@@ -1838,7 +1838,7 @@
       /*group_sequence=*/0,
       /*object_sequence=*/0,
       /*publisher_priority=*/0,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/0,
       /*payload_length=*/8,
@@ -2473,7 +2473,7 @@
       /*group_id, object_id=*/0,
       0,
       /*publisher_priority=*/128,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*status=*/MoqtObjectStatus::kNormal,
       /*subgroup=*/0,
       /*payload_length=*/3,
@@ -2623,7 +2623,7 @@
       /*group_id, object_id=*/0,
       0,
       /*publisher_priority=*/128,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*status=*/MoqtObjectStatus::kNormal,
       /*subgroup=*/0,
       /*payload_length=*/3,
@@ -2694,7 +2694,7 @@
       /*group_id, object_id=*/0,
       0,
       /*publisher_priority=*/128,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*status=*/MoqtObjectStatus::kNormal,
       /*subgroup=*/0,
       /*payload_length=*/3,
@@ -2785,7 +2785,7 @@
       /*group_id, object_id=*/0,
       0,
       /*publisher_priority=*/128,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*status=*/MoqtObjectStatus::kNormal,
       /*subgroup=*/0,
       /*payload_length=*/6,
@@ -3216,7 +3216,7 @@
       /*group_id=*/0,
       /*object_id=*/0,
       /*publisher_priority=*/7,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kGroupDoesNotExist,
       /*subgroup_id=*/0,
       /*payload_length=*/0,
@@ -3273,7 +3273,7 @@
       /*group_id=*/0,
       /*object_id=*/0,
       /*publisher_priority=*/7,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kGroupDoesNotExist,
       /*subgroup_id=*/0,
       /*payload_length=*/0,
@@ -3327,7 +3327,7 @@
       /*group_id=*/0,
       /*object_id=*/0,
       /*publisher_priority=*/7,
-      std::vector<MoqtExtensionHeader>(),
+      /*extension_headers=*/"",
       /*object_status=*/MoqtObjectStatus::kGroupDoesNotExist,
       /*subgroup_id=*/0,
       /*payload_length=*/0,
diff --git a/quiche/quic/moqt/moqt_track_test.cc b/quiche/quic/moqt/moqt_track_test.cc
index 13a6700..1a9fbc4 100644
--- a/quiche/quic/moqt/moqt_track_test.cc
+++ b/quiche/quic/moqt/moqt_track_test.cc
@@ -4,9 +4,11 @@
 
 #include "quiche/quic/moqt/moqt_track.h"
 
+#include <cstdint>
 #include <memory>
 #include <optional>
 #include <utility>
+#include <vector>
 
 #include "absl/memory/memory.h"
 #include "absl/status/status.h"
@@ -174,14 +176,7 @@
   PublishedObject object;
   EXPECT_EQ(fetch_task_->GetNextObject(object),
             MoqtFetchTask::GetNextObjectResult::kPending);
-  MoqtObject new_object = {1,
-                           3,
-                           0,
-                           128,
-                           std::vector<MoqtExtensionHeader>(),
-                           MoqtObjectStatus::kNormal,
-                           0,
-                           6};
+  MoqtObject new_object = {1, 3, 0, 128, "", MoqtObjectStatus::kNormal, 0, 6};
   bool got_object = false;
   fetch_task_->SetObjectAvailableCallback([&]() {
     got_object = true;
diff --git a/quiche/quic/moqt/test_tools/moqt_test_message.h b/quiche/quic/moqt/test_tools/moqt_test_message.h
index 9da46f0..1eca5c6 100644
--- a/quiche/quic/moqt/test_tools/moqt_test_message.h
+++ b/quiche/quic/moqt/test_tools/moqt_test_message.h
@@ -11,6 +11,7 @@
 #include <memory>
 #include <optional>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "absl/strings/string_view.h"
@@ -26,6 +27,9 @@
 
 namespace moqt::test {
 
+inline constexpr absl::string_view kDefaultExtensionBlob(
+    "\x00\x0c\x01\x03\x66\x6f\x6f", 7);
+
 // Base class containing a wire image and the corresponding structured
 // representation of an example of each message. It allows parser and framer
 // tests to iterate through all message types without much specialized code.
@@ -164,7 +168,7 @@
 class QUICHE_NO_EXPORT ObjectMessage : public TestMessageBase {
  public:
   bool EqualFieldValues(MessageStructuredData& values) const override {
-    auto cast = std::get<MoqtObject>(values);
+    auto cast = std::move(std::get<MoqtObject>(values));
     if (cast.track_alias != object_.track_alias) {
       QUIC_LOG(INFO) << "OBJECT Track ID mismatch";
       return false;
@@ -181,21 +185,10 @@
       QUIC_LOG(INFO) << "OBJECT Publisher Priority mismatch";
       return false;
     }
-    if (cast.extension_headers.size() != object_.extension_headers.size()) {
-      QUIC_LOG(INFO) << "OBJECT Extension Header size mismatch";
+    if (cast.extension_headers != object_.extension_headers) {
+      QUIC_LOG(INFO) << "OBJECT Extension Header mismatch";
       return false;
     }
-    for (size_t i = 0; i < cast.extension_headers.size(); ++i) {
-      if (cast.extension_headers[i].type != object_.extension_headers[i].type) {
-        QUIC_LOG(INFO) << "OBJECT Extension Header type mismatch";
-        return false;
-      }
-      if (cast.extension_headers[i].value !=
-          object_.extension_headers[i].value) {
-        QUIC_LOG(INFO) << "OBJECT Extension Header value mismatch";
-        return false;
-      }
-    }
     if (cast.object_status != object_.object_status) {
       QUIC_LOG(INFO) << "OBJECT Object Status mismatch";
       return false;
@@ -221,10 +214,7 @@
       /*group_id*/ 5,
       /*object_id=*/6,
       /*publisher_priority=*/7,
-      std::vector<MoqtExtensionHeader>(
-          {MoqtExtensionHeader(0, absl::variant<uint64_t, std::string>(12ULL)),
-           MoqtExtensionHeader(1,
-                               absl::variant<uint64_t, std::string>("foo"))}),
+      std::string(kDefaultExtensionBlob),
       /*object_status=*/MoqtObjectStatus::kNormal,
       /*subgroup_id=*/std::nullopt,
       /*payload_length=*/3,
@@ -238,13 +228,13 @@
   }
 
   void ExpandVarints() override {
-    ExpandVarintsImpl("vvvv-vvvvv---v---", false);
+    ExpandVarintsImpl("vvvv-v-------v---", false);
   }
 
  private:
   uint8_t raw_packet_[17] = {
       0x01, 0x04, 0x05, 0x06,  // varints
-      0x07, 0x02,              // publisher priority, no extensions
+      0x07, 0x07,              // publisher priority, 7B extensions
       0x00, 0x0c, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // extensions
       0x03, 0x66, 0x6f, 0x6f,                    // payload = "foo"
   };
@@ -256,15 +246,17 @@
     SetWireImage(raw_packet_, sizeof(raw_packet_));
     object_.object_status = MoqtObjectStatus::kEndOfGroup;
     object_.payload_length = 0;
-    object_.extension_headers.clear();
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vvvv-v", false); }
+  void ExpandVarints() override { ExpandVarintsImpl("vvvv-v-------v", false); }
 
  private:
-  uint8_t raw_packet_[6] = {
-      0x02, 0x04, 0x05, 0x06,  // varints
-      0x07, 0x03,              // publisher priority, kEndOfGroup
+  uint8_t raw_packet_[14] = {
+      0x02, 0x04, 0x05, 0x06,                    // varints
+      0x07,                                      // publisher priority
+      0x07,                                      // 7B extensions
+      0x00, 0x0c, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // extensions
+      0x03,                                      // kEndOfGroup
   };
 };
 
@@ -277,7 +269,9 @@
     object_.subgroup_id = 8;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vvvv-vvvvvv---v", false); }
+  void ExpandVarints() override {
+    ExpandVarintsImpl("vvvv-vv-------v---", false);
+  }
 
   bool SetPayloadLength(uint8_t payload_length) {
     if (payload_length > 63) {
@@ -295,7 +289,7 @@
       0x04,                                      // type field
       0x04, 0x05, 0x08,                          // varints
       0x07,                                      // publisher priority
-      0x06, 0x02,                                // object ID, 2 extensions
+      0x06, 0x07,                                // object ID, 7B extensions
       0x00, 0x0c, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // extensions
       0x03, 0x66, 0x6f, 0x6f,                    // payload = "foo"
   };
@@ -310,11 +304,11 @@
     object_.object_id = 9;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vvvvvv---v", false); }
+  void ExpandVarints() override { ExpandVarintsImpl("vv-------v---", false); }
 
  private:
   uint8_t raw_packet_[13] = {
-      0x09, 0x02,                                // object ID; 2 extensions
+      0x09, 0x07,                                // object ID; 7B extensions
       0x00, 0x0c, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // extensions
       0x03, 0x62, 0x61, 0x72,                    // payload = "bar"
   };
@@ -328,7 +322,7 @@
   }
 
   void ExpandVarints() override {
-    ExpandVarintsImpl("vvvvv-vvvvv---v---", false);
+    ExpandVarintsImpl("vvvvv-v-------v---", false);
   }
 
   bool SetPayloadLength(uint8_t payload_length) {
@@ -348,7 +342,7 @@
       0x04,              // subscribe ID
                          // object middler:
       0x05, 0x08, 0x06,  // sequence
-      0x07, 0x02,        // publisher priority, 2 extensions
+      0x07, 0x07,        // publisher priority, 7B extensions
       0x00, 0x0c, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // extensions
       0x03, 0x66, 0x6f, 0x6f,                    // payload = "foo"
   };
@@ -363,11 +357,13 @@
     object_.object_id = 9;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vvv-vv---", false); }
+  void ExpandVarints() override {
+    ExpandVarintsImpl("vvv-v-------v---", false);
+  }
 
  private:
   uint8_t raw_packet_[16] = {
-      0x05, 0x08, 0x09, 0x07, 0x02,              // Object metadata
+      0x05, 0x08, 0x09, 0x07, 0x07,              // Object metadata
       0x00, 0x0c, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // extensions
       0x03, 0x62, 0x61, 0x72,                    // Payload = "bar"
   };