Send LARGEST_OBJECT when REQUEST_UPDATE sets Forward=1 or moves the filter end back.

If forward is set to 1, re-initialize the subscription filter with the new LARGEST_OBJECT.

Also, fixes a problem where the publisher can respond to REQUEST_UPDATE before the initial SUBSCRIBE.

PiperOrigin-RevId: 994003251
diff --git a/quiche/quic/moqt/moqt_bidi_stream.cc b/quiche/quic/moqt/moqt_bidi_stream.cc
index 15167c8..461b636 100644
--- a/quiche/quic/moqt/moqt_bidi_stream.cc
+++ b/quiche/quic/moqt/moqt_bidi_stream.cc
@@ -86,7 +86,7 @@
   request_update.request_id = request_id;
   request_update.existing_request_id = existing_request_id;
   request_update.parameters = parameters;
-  request_update_queue_.Enqueue(parameters, std::move(callback));
+  outgoing_update_queue_.Enqueue(parameters, std::move(callback));
   return SendOrBufferMessage(framer_->SerializeRequestUpdate(request_update),
                              /*fin=*/false);
 }
diff --git a/quiche/quic/moqt/moqt_bidi_stream.h b/quiche/quic/moqt/moqt_bidi_stream.h
index 0e59ab9..4188d2a 100644
--- a/quiche/quic/moqt/moqt_bidi_stream.h
+++ b/quiche/quic/moqt/moqt_bidi_stream.h
@@ -26,6 +26,7 @@
 #include "quiche/common/platform/api/quiche_logging.h"
 #include "quiche/common/quiche_buffer_allocator.h"
 #include "quiche/common/quiche_callbacks.h"
+#include "quiche/common/quiche_circular_deque.h"
 #include "quiche/web_transport/web_transport.h"
 
 namespace moqt {
@@ -149,6 +150,17 @@
   webtransport::StreamId stream_id() const {
     return stream() != nullptr ? stream()->GetStreamId() : 0;
   }
+  // If there are incoming REQUEST_UPDATEs buffered for processing, returns the
+  // parameters saved for the oldest one. Also pops it from the queue.
+  // Otherwise, returns nullopt.
+  std::optional<MessageParameters> NextIncomingUpdate() {
+    if (incoming_update_queue_.empty()) {
+      return std::nullopt;
+    }
+    MessageParameters parameters = incoming_update_queue_.front();
+    incoming_update_queue_.pop_front();
+    return parameters;
+  }
 
   absl::Status OnControlMessage(const MoqtGoAway& message);
 
@@ -163,8 +175,12 @@
       const MoqtRawControlMessage& message) = 0;
 
   MoqtRequestUpdateQueue& request_update_queue() {
-    return request_update_queue_;
+    return outgoing_update_queue_;
   }
+  void QueueIncomingUpdate(const MessageParameters& parameters) {
+    incoming_update_queue_.push_back(parameters);
+  }
+  bool IncomingUpdatesQueued() const { return !incoming_update_queue_.empty(); }
 
   // Terminates the MoQT session due to a fatal error encountered.
   void OnFatalError(absl::Status status);
@@ -187,7 +203,10 @@
   std::optional<MoqtControlStreamParser> stream_parser_;
   MoqtControlMessageParser message_parser_;
   MoqtControlMessageQueue outgoing_message_queue_;
-  MoqtRequestUpdateQueue request_update_queue_;
+  MoqtRequestUpdateQueue outgoing_update_queue_;
+  // Incoming REQUEST_UPDATEs that cannot be handled synchronously (possibly
+  // because the OK for the original request has not been sent)
+  quiche::QuicheCircularDeque<MessageParameters> incoming_update_queue_;
   SessionErrorCallback session_error_callback_;
   absl::Status stream_status_ = absl::OkStatus();
   bool received_goaway_ = false;
diff --git a/quiche/quic/moqt/moqt_bidi_stream_test.cc b/quiche/quic/moqt/moqt_bidi_stream_test.cc
index e4b3973..e56ea76 100644
--- a/quiche/quic/moqt/moqt_bidi_stream_test.cc
+++ b/quiche/quic/moqt/moqt_bidi_stream_test.cc
@@ -58,6 +58,9 @@
     }
     return status;
   }
+
+  using MoqtBidiStreamBase::IncomingUpdatesQueued;
+  using MoqtBidiStreamBase::QueueIncomingUpdate;
 };
 
 class MoqtBidiStreamTest : public quiche::test::QuicheTest {
@@ -228,4 +231,25 @@
   EXPECT_FALSE(stream_->QueueIsFull());
 }
 
+TEST_F(MoqtBidiStreamTest, IncomingUpdateQueue) {
+  EXPECT_FALSE(stream_->IncomingUpdatesQueued());
+  EXPECT_EQ(stream_->NextIncomingUpdate(), std::nullopt);
+
+  MessageParameters update1;
+  update1.subscriber_priority = 10;
+  MessageParameters update2;
+  update2.subscriber_priority = 20;
+
+  stream_->QueueIncomingUpdate(update1);
+  EXPECT_TRUE(stream_->IncomingUpdatesQueued());
+  stream_->QueueIncomingUpdate(update2);
+  EXPECT_TRUE(stream_->IncomingUpdatesQueued());
+
+  EXPECT_EQ(stream_->NextIncomingUpdate(), update1);
+  EXPECT_TRUE(stream_->IncomingUpdatesQueued());
+  EXPECT_EQ(stream_->NextIncomingUpdate(), update2);
+  EXPECT_FALSE(stream_->IncomingUpdatesQueued());
+  EXPECT_EQ(stream_->NextIncomingUpdate(), std::nullopt);
+}
+
 }  // namespace moqt::test
diff --git a/quiche/quic/moqt/moqt_fetch_stream.cc b/quiche/quic/moqt/moqt_fetch_stream.cc
index 78b97d4..1bac6fc 100644
--- a/quiche/quic/moqt/moqt_fetch_stream.cc
+++ b/quiche/quic/moqt/moqt_fetch_stream.cc
@@ -297,6 +297,25 @@
         delivery_order, std::move(response_callback));
   } else {
     // Joining Fetch.
+    // TODO(martinduke): There are some gaps in this implementation that are
+    // relevant to draft-18 Joining FETCH but not later drafts, where it occurs
+    // on the SUBSCRIBE stream and therefore does not need a reference request
+    // ID:
+    // 1. draft-18 is ambiguous about whether to use the original request ID or
+    // the request ID of a REQUEST_UPDATE. Our current request ID tracking data
+    // structures and callbacks are not well-equipped to track multiple request
+    // IDs per subscription. There are good reasons to use the REQUEST_UPDATE
+    // ID: it can prevent a FETCH from failing because forward is set to zero.
+    // However, our draft-18 implementation will fail any joining FETCH that
+    // beats such a REQUEST_UPDATE.
+    // 2. It's hard to distinguish between a request ID that used to exist and
+    // one that has yet to arrive. It would be best to use
+    // MoqtSession::next_incoming_request_id to guess if the request is new or
+    // old, and buffer the stream if it's new.
+    //
+    // draft-18 is a candidate for long term support, but it may not be worth it
+    // to fix problems that don't exist in future drafts if joining FETCH is
+    // not important to early-deployed use cases.
     uint64_t joining_request_id =
         std::holds_alternative<JoiningFetchRelative>(message.fetch)
             ? std::get<JoiningFetchRelative>(message.fetch).joining_request_id
diff --git a/quiche/quic/moqt/moqt_key_value_pair.cc b/quiche/quic/moqt/moqt_key_value_pair.cc
index ce63ffe..1ff4b4a 100644
--- a/quiche/quic/moqt/moqt_key_value_pair.cc
+++ b/quiche/quic/moqt/moqt_key_value_pair.cc
@@ -13,6 +13,7 @@
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/moqt/moqt_priority.h"
+#include "quiche/quic/moqt/moqt_types.h"
 #include "quiche/common/platform/api/quiche_bug_tracker.h"
 #include "quiche/common/platform/api/quiche_logging.h"
 
@@ -54,19 +55,22 @@
   switch (type_) {
     case MoqtFilterType::kAbsoluteStart:
     case MoqtFilterType::kAbsoluteRange:
-      return;
+      break;
     case MoqtFilterType::kNextGroupStart:
       if (largest_object.has_value()) {
         start_ = Location(largest_object->group + 1, 0);
+      } else {
+        start_ = Location(0, 0);
       }
       break;
     case MoqtFilterType::kLargestObject:
       if (largest_object.has_value()) {
         start_ = largest_object->Next();
+      } else {
+        start_ = Location(0, 0);
       }
       break;
   }
-  type_ = MoqtFilterType::kAbsoluteStart;
 }
 
 void MessageParameters::Update(const MessageParameters& other) {
diff --git a/quiche/quic/moqt/moqt_key_value_pair.h b/quiche/quic/moqt/moqt_key_value_pair.h
index 5eef4d5..5f158a3 100644
--- a/quiche/quic/moqt/moqt_key_value_pair.h
+++ b/quiche/quic/moqt/moqt_key_value_pair.h
@@ -124,23 +124,20 @@
   }
 
   MoqtFilterType type() const { return type_; }
-  Location start() const { return start_; }
+  Location start() const { return start_.value_or(Location(0, 0)); }
   uint64_t end_group() const { return end_group_; }
   // If true, the filter does not depend on knowing LargestObject, or we
   // already know LargestObject. A Subscriber cannot have an unknown window,
   // because it can't process an object without getting the track alias from
   // the SubscribeOk.
-  bool WindowKnown() const {
-    return type_ == MoqtFilterType::kAbsoluteStart ||
-           type_ == MoqtFilterType::kAbsoluteRange;
-  }
+  bool WindowKnown() const { return start_.has_value(); }
   // Publishers should not call InWindow() if !WindowKnown(), as they should
   // not forward objects without knowing the window.
   bool InWindow(Location location) const {
-    return location >= start_ && location.group <= end_group_;
+    return location >= start() && location.group <= end_group_;
   }
   bool InWindow(uint64_t group) const {
-    return group >= start_.group && group <= end_group_;
+    return (group >= start().group) && group <= end_group_;
   }
   // Update the filter when LargestObject is learned. If the type is
   // LargestObject or NextGroupStart, changes the type to AbsoluteStart.
@@ -150,8 +147,7 @@
 
  private:
   MoqtFilterType type_;
-  // These could be std::optional, but it would just add to the class size.
-  Location start_ = Location(0, 0);
+  std::optional<Location> start_;
   uint64_t end_group_ = kMaxGroupId;
 };
 
diff --git a/quiche/quic/moqt/moqt_key_value_pair_test.cc b/quiche/quic/moqt/moqt_key_value_pair_test.cc
index 9e21fd8..6400ef8 100644
--- a/quiche/quic/moqt/moqt_key_value_pair_test.cc
+++ b/quiche/quic/moqt/moqt_key_value_pair_test.cc
@@ -10,8 +10,8 @@
 #include "absl/status/status.h"
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_time.h"
-#include "quiche/quic/moqt/moqt_error.h"
 #include "quiche/quic/moqt/moqt_priority.h"
+#include "quiche/quic/moqt/moqt_types.h"
 #include "quiche/quic/platform/api/quic_test.h"
 #include "quiche/common/test_tools/quiche_test_utils.h"
 
@@ -70,7 +70,7 @@
   EXPECT_FALSE(filter.WindowKnown());
   filter.OnLargestObject(Location(3, 6));
   EXPECT_TRUE(filter.WindowKnown());
-  EXPECT_EQ(filter.type(), (MoqtFilterType::kAbsoluteStart));
+  EXPECT_EQ(filter.type(), (MoqtFilterType::kNextGroupStart));
   EXPECT_TRUE(filter.InWindow(Location(4, 0)));
   EXPECT_FALSE(filter.InWindow(Location(3, 7)));
   EXPECT_TRUE(filter.InWindow(4));
@@ -82,7 +82,7 @@
   EXPECT_EQ(filter.type(), (MoqtFilterType::kLargestObject));
   EXPECT_FALSE(filter.WindowKnown());
   filter.OnLargestObject(Location(3, 6));
-  EXPECT_EQ(filter.type(), (MoqtFilterType::kAbsoluteStart));
+  EXPECT_EQ(filter.type(), (MoqtFilterType::kLargestObject));
   EXPECT_TRUE(filter.WindowKnown());
   EXPECT_TRUE(filter.InWindow(Location(4, 0)));
   EXPECT_TRUE(filter.InWindow(Location(3, 7)));
@@ -97,7 +97,7 @@
   EXPECT_EQ(filter.type(), (MoqtFilterType::kLargestObject));
   EXPECT_FALSE(filter.WindowKnown());
   filter.OnLargestObject(std::nullopt);
-  EXPECT_EQ(filter.type(), (MoqtFilterType::kAbsoluteStart));
+  EXPECT_EQ(filter.type(), (MoqtFilterType::kLargestObject));
   EXPECT_TRUE(filter.WindowKnown());
   EXPECT_TRUE(filter.InWindow(Location(0, 0)));
   EXPECT_TRUE(filter.InWindow(0));
diff --git a/quiche/quic/moqt/moqt_live_publisher.cc b/quiche/quic/moqt/moqt_live_publisher.cc
index a67ddad..ee32d49 100644
--- a/quiche/quic/moqt/moqt_live_publisher.cc
+++ b/quiche/quic/moqt/moqt_live_publisher.cc
@@ -15,6 +15,7 @@
 #include "absl/base/nullability.h"
 #include "absl/container/btree_map.h"
 #include "absl/container/flat_hash_set.h"
+#include "absl/status/status.h"
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_clock.h"
 #include "quiche/quic/moqt/moqt_bidi_stream.h"
@@ -72,15 +73,39 @@
   }
 }
 
-void LivePublisher::Update(const MessageParameters& parameters) {
-  if (!track_publisher_->UpdateObjectListener(this, parameters).ok()) {
+void LivePublisher::Update(const MessageParameters& parameters,
+                           bool from_request_ok) {
+  absl::Status pub_status =
+      track_publisher_->UpdateObjectListener(this, parameters);
+  if (!pub_status.ok()) {
+    if (!from_request_ok) {
+      bidi_stream_->CheckStatus(
+          bidi_stream_->SendRequestError(StatusToMoqtRequestError(pub_status)));
+    } else {
+      bidi_stream_->Reset(StatusToMoqtStreamError(pub_status));
+    }
     return;
   }
+  MessageParameters response;
   // TODO(martinduke): If the subscribe window has shrunk, close any streams
   // that are now outside the window. Also send PUBLISH_DONE if now done.
   MoqtPriority old_priority =
       parameters_.subscriber_priority.value_or(kDefaultSubscriberPriority);
+  // Does this update set forward to true?
+  bool forward_to_1 = !parameters_.forward() &&
+                      parameters.forward_has_value() && parameters.forward();
+  response.largest_object = track_publisher_->largest_location();
+  parameters_.largest_object = response.largest_object;
   parameters_.Update(parameters);
+  if (parameters_.subscription_filter.has_value() &&
+      parameters_.largest_object.has_value() &&
+      (!parameters_.subscription_filter->WindowKnown() || forward_to_1)) {
+    // Initialize the filter if:
+    // - Forward is going 0->1 (this could be a re-initialization)
+    // - This the first LARGEST_OBJECT sent.
+    // - There's a new filter that has to be initialized from LARGEST_OBJECT.
+    parameters_.subscription_filter->OnLargestObject(response.largest_object);
+  }
   if (parameters.subscriber_priority.has_value()) {  // priority changed.
     MoqtPriority new_priority = *parameters.subscriber_priority;
     // Reprioritize all active streams.
@@ -93,21 +118,19 @@
           absl::down_cast<OutgoingSubgroupStream*>(stream->visitor());
       outgoing_stream->UpdatePriority(new_priority);
     }
-    if (pending_streams_.empty()) {
-      return;
+    if (!pending_streams_.empty() && visitor() != nullptr) {
+      // Tell the session that pending stream priority has changed.
+      MoqtPriority publisher_priority =
+          pending_streams_.rbegin()->second.publisher_priority.value_or(
+              track_publisher_->properties().default_publisher_priority());
+      MoqtTrackPriority old_track_priority = {old_priority, publisher_priority};
+      visitor()->UpdateTrackPriority(
+          track_publisher_->GetTrackName(), old_track_priority,
+          MoqtTrackPriority{new_priority, publisher_priority});
     }
-    // Tell the session that pending stream priority has changed.
-    MoqtPriority publisher_priority =
-        pending_streams_.rbegin()->second.publisher_priority.value_or(
-            track_publisher_->properties().default_publisher_priority());
-    MoqtTrackPriority old_track_priority = {old_priority, publisher_priority};
-    if (visitor() == nullptr) {
-      return;
-    }
-    visitor()->UpdateTrackPriority(
-        track_publisher_->GetTrackName(), old_track_priority,
-        MoqtTrackPriority{new_priority, publisher_priority});
-    // Don't bother to update all the pending stream send orders.
+  }
+  if (!from_request_ok) {
+    bidi_stream_->CheckStatus(bidi_stream_->SendRequestOk(response));
   }
 }
 
@@ -148,6 +171,19 @@
       subscribe_ok.properties.default_publisher_priority();
   bidi_stream_->SendOrBufferMessageOrFatal(
       framer_.SerializeSubscribeOk(subscribe_ok));
+  // It is now safe to process buffered REQUEST_UPDATEs.
+  std::optional<MessageParameters> next_update =
+      bidi_stream_->NextIncomingUpdate();
+  // Get a weak_pointer to LivePublisher in case it is destroyed by Update().
+  quiche::QuicheWeakPtr<LivePublisherInterface> self =
+      weak_ptr_factory_.Create();
+  while (self.IsValid() && next_update.has_value()) {
+    Update(*next_update, /*from_request_ok=*/false);
+    if (!self.IsValid()) {
+      return;
+    }
+    next_update = bidi_stream_->NextIncomingUpdate();
+  }
   // TODO(martinduke): If we buffer objects that arrived previously, the arrival
   // of the track alias disambiguates what subscription they belong to. Send
   // them.
diff --git a/quiche/quic/moqt/moqt_live_publisher.h b/quiche/quic/moqt/moqt_live_publisher.h
index 6bbdb00..c2a5c95 100644
--- a/quiche/quic/moqt/moqt_live_publisher.h
+++ b/quiche/quic/moqt/moqt_live_publisher.h
@@ -178,8 +178,9 @@
   // MoqtObjectPublisher implementation.
   void OnCanCreateNewUniStream();
 
-  // Called when the parameters_ needs an update.
-  void Update(const MessageParameters& parameters);
+  // Called when the parameters_ need an update and will generate REQUEST_OK or
+  // REQUEST_ERROR, unless from_request_ok is true (which requires no response).
+  void Update(const MessageParameters& parameters, bool from_request_ok);
 
   bool can_have_joining_fetch() const { return parameters_.forward(); }
 
diff --git a/quiche/quic/moqt/moqt_live_publisher_test.cc b/quiche/quic/moqt/moqt_live_publisher_test.cc
index ed67d2a..299a191 100644
--- a/quiche/quic/moqt/moqt_live_publisher_test.cc
+++ b/quiche/quic/moqt/moqt_live_publisher_test.cc
@@ -34,6 +34,7 @@
 #include "quiche/quic/moqt/moqt_trace_recorder.h"
 #include "quiche/quic/moqt/moqt_types.h"
 #include "quiche/quic/moqt/test_tools/mock_moqt_session.h"
+#include "quiche/quic/moqt/test_tools/moqt_framer_utils.h"
 #include "quiche/quic/moqt/test_tools/moqt_mock_visitor.h"
 #include "quiche/quic/moqt/test_tools/moqt_session_peer.h"
 #include "quiche/quic/platform/api/quic_test.h"
@@ -83,6 +84,7 @@
     return absl::OkStatus();
   }
   void Detach() override { detached_ = true; }
+  using MoqtBidiStreamBase::QueueIncomingUpdate;
   bool detached_ = false;
 };
 
@@ -291,6 +293,50 @@
   publisher_->OnNewObjectAvailable(Location(1, 3), std::nullopt, 64);
 }
 
+TEST_F(LivePublisherTest, OnSubscribeAcceptedWithQueuedUpdates) {
+  MessageParameters update1;
+  update1.delivery_timeout = quic::QuicTimeDelta::FromSeconds(5);
+  update1.set_forward(false);
+  bidi_stream_.QueueIncomingUpdate(update1);
+
+  MessageParameters update2;
+  update2.set_forward(true);
+  update2.subscription_filter.emplace(MoqtFilterType::kLargestObject);
+  bidi_stream_.QueueIncomingUpdate(update2);
+
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+  EXPECT_CALL(*track_publisher_, largest_location())
+      .WillOnce(Return(std::nullopt))
+      .WillOnce(Return(Location(1, 2)))
+      .WillOnce(Return(Location(2, 5)));
+  EXPECT_CALL(*track_publisher_, expiration)
+      .WillOnce(Return(quic::QuicTimeDelta::FromSeconds(10)));
+  EXPECT_CALL(*track_publisher_, properties)
+      .WillRepeatedly(ReturnRef(properties_));
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(ControlMessageOfType(MoqtMessageType::kSubscribeOk), _))
+      .WillOnce(Return(absl::OkStatus()));
+  MessageParameters expected_ok;
+  expected_ok.largest_object = Location(1, 2);
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk(expected_ok)), _))
+      .WillOnce(Return(absl::OkStatus()));
+  expected_ok.largest_object = Location(2, 5);
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk(expected_ok)), _))
+      .WillOnce(Return(absl::OkStatus()));
+
+  publisher_->OnSubscribeAccepted();
+  EXPECT_TRUE(publisher_->established());
+  EXPECT_EQ(publisher_->parameters().delivery_timeout,
+            quic::QuicTimeDelta::FromSeconds(5));
+  EXPECT_TRUE(publisher_->parameters().forward());
+  EXPECT_EQ(publisher_->parameters().largest_object, Location(2, 5));
+  ASSERT_TRUE(publisher_->parameters().subscription_filter.has_value());
+  EXPECT_EQ(publisher_->parameters().subscription_filter->start(),
+            Location(2, 6));
+}
+
 TEST_F(LivePublisherTest, OnSubscribeRejected) {
   EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
   EXPECT_CALL(mock_bidi_stream_,
@@ -303,19 +349,108 @@
 TEST_F(LivePublisherTest, Update) {
   MessageParameters new_params;
   new_params.delivery_timeout = quic::QuicTimeDelta::FromSeconds(5);
-  publisher_->Update(new_params);
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk()), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(new_params, false);
+  EXPECT_EQ(publisher_->parameters().delivery_timeout,
+            quic::QuicTimeDelta::FromSeconds(5));
 
   // Changing forward preference updates can_have_joining_fetch_
   new_params.set_forward(false);
-  publisher_->Update(new_params);
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk()), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(new_params, false);
   EXPECT_FALSE(publisher_->parameters().forward());
   EXPECT_FALSE(publisher_->can_have_joining_fetch());
+
+  // Setting a LargestObject filter while forward=false, then turning forward=
+  // true initializes the filter using the latest largest_location.
+  publisher_->parameters().subscription_filter =
+      SubscriptionFilter(MoqtFilterType::kLargestObject);
+  MessageParameters forward_on_params;
+  forward_on_params.set_forward(true);
+  EXPECT_CALL(*track_publisher_, largest_location())
+      .WillOnce(Return(Location(2, 4)));
+  MessageParameters expected_ok;
+  expected_ok.largest_object = Location(2, 4);
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk{expected_ok}), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(forward_on_params, false);
+  EXPECT_TRUE(publisher_->parameters().forward());
+  EXPECT_TRUE(publisher_->can_have_joining_fetch());
+  EXPECT_EQ(publisher_->parameters().largest_object, Location(2, 4));
+  ASSERT_TRUE(publisher_->parameters().subscription_filter.has_value());
+  EXPECT_EQ(publisher_->parameters().subscription_filter->start(),
+            Location(2, 5));
+}
+
+TEST_F(LivePublisherTest, UpdateSubscriptionFilterAndEndGroup) {
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+
+  // Adding a new kLargestObject filter when forward is already true initializes
+  // the filter and sends LARGEST_OBJECT in REQUEST_OK.
+  MessageParameters filter_params;
+  filter_params.subscription_filter.emplace(MoqtFilterType::kLargestObject);
+  EXPECT_CALL(*track_publisher_, largest_location())
+      .WillOnce(Return(Location(3, 1)));
+  MessageParameters expected_ok1;
+  expected_ok1.largest_object = Location(3, 1);
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk{expected_ok1}), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(filter_params, false);
+  ASSERT_TRUE(publisher_->parameters().subscription_filter.has_value());
+  EXPECT_EQ(publisher_->parameters().subscription_filter->start(),
+            Location(3, 2));
+
+  // Set an AbsoluteRange filter and then extend end_group.
+  publisher_->parameters().subscription_filter =
+      SubscriptionFilter(Location(1, 0), 5);
+  MessageParameters extend_params;
+  extend_params.subscription_filter = SubscriptionFilter(Location(1, 0), 8);
+  EXPECT_CALL(*track_publisher_, largest_location())
+      .WillOnce(Return(Location(4, 2)));
+  MessageParameters expected_ok2;
+  expected_ok2.largest_object = Location(4, 2);
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk{expected_ok2}), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(extend_params, false);
+  EXPECT_EQ(publisher_->parameters().subscription_filter->end_group(), 8);
+  EXPECT_EQ(publisher_->parameters().largest_object, Location(4, 2));
+}
+
+TEST_F(LivePublisherTest, UpdateRejectedByPublisher) {
+  MessageParameters new_params;
+  new_params.delivery_timeout = quic::QuicTimeDelta::FromSeconds(5);
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+
+  // When from_request_ok is false, sends REQUEST_ERROR.
+  EXPECT_CALL(*track_publisher_, UpdateObjectListener(publisher_.get(), _))
+      .WillOnce(Return(absl::InvalidArgumentError("invalid update")));
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(ControlMessageOfType(MoqtMessageType::kRequestError), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(new_params, /*from_request_ok=*/false);
+  EXPECT_EQ(publisher_->parameters().delivery_timeout,
+            quic::QuicTimeDelta::FromSeconds(1));
+
+  // When from_request_ok is true, resets the bidi stream.
+  EXPECT_CALL(*track_publisher_, UpdateObjectListener(publisher_.get(), _))
+      .WillOnce(Return(absl::InvalidArgumentError("invalid update")));
+  EXPECT_CALL(mock_bidi_stream_, ResetWithUserCode);
+  EXPECT_CALL(mock_bidi_stream_, SendStopSending);
+  publisher_->Update(new_params, /*from_request_ok=*/true);
 }
 
 TEST_F(LivePublisherTest, UpdatePriorityNoStreams) {
   MessageParameters new_params;
   new_params.subscriber_priority = 20;
-  publisher_->Update(new_params);
+  publisher_->Update(new_params, true);
   EXPECT_EQ(publisher_->parameters().subscriber_priority, 20);
 }
 
@@ -329,7 +464,11 @@
                                             std::optional<MoqtTrackPriority>(
                                                 {subscriber_priority(), 64}),
                                             MoqtTrackPriority{20, 64}));
-  publisher_->Update(new_params);
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk()), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(new_params, false);
 }
 
 TEST_F(LivePublisherTest, UpdatePriorityWithActiveStreams) {
@@ -339,13 +478,17 @@
   MessageParameters new_params;
   new_params.subscriber_priority = 20;
   EXPECT_CALL(mock_uni_stream_, SetPriority);
-  publisher_->Update(new_params);
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk()), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(new_params, false);
 }
 
 TEST_F(LivePublisherTest, OnNewObjectAvailableNotInWindow) {
   MessageParameters params;
   params.subscription_filter = SubscriptionFilter(Location(10, 0), 10);
-  publisher_->Update(params);
+  publisher_->Update(params, true);
   EXPECT_CALL(*track_publisher_, GetCachedObject).Times(0);
   publisher_->OnNewObjectAvailable(Location(5, 0), 0, 128);
 }
@@ -373,13 +516,17 @@
   // Not in window
   MessageParameters params;
   params.subscription_filter = SubscriptionFilter(Location(10, 0), 10);
-  publisher_->Update(params);
+  publisher_->Update(params, true);
   EXPECT_CALL(webtrans_, GetStreamById).Times(0);
   EXPECT_CALL(mock_uni_stream_, Writev).Times(0);
   publisher_->OnNewFinAvailable(Location(0, 0), 0);
 
   // In window but no stream
-  publisher_->Update(parameters_);
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk()), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(parameters_, false);
   EXPECT_CALL(webtrans_, GetStreamById).Times(0);
   EXPECT_CALL(mock_uni_stream_, Writev).Times(0);
   publisher_->OnNewFinAvailable(Location(10, 10), 0);
@@ -410,12 +557,16 @@
   // Not in window
   MessageParameters params;
   params.subscription_filter = SubscriptionFilter(Location(10, 0), 10);
-  publisher_->Update(params);
+  publisher_->Update(params, true);
   EXPECT_CALL(webtrans_, GetStreamById).Times(0);
   publisher_->OnSubgroupAbandoned(1, 0, 17);
 
   // In window but no stream
-  publisher_->Update(parameters_);
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk()), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(parameters_, false);
   EXPECT_CALL(webtrans_, GetStreamById).Times(0);
   publisher_->OnSubgroupAbandoned(1, 0, 17);
 }
@@ -424,12 +575,16 @@
   // Not in window
   MessageParameters params;
   params.subscription_filter = SubscriptionFilter(Location(10, 0), 10);
-  publisher_->Update(params);
+  publisher_->Update(params, true);
   EXPECT_CALL(webtrans_, GetStreamById).Times(0);
   publisher_->OnGroupAbandoned(1);
 
   // In window
-  publisher_->Update(parameters_);
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk()), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(parameters_, false);
   EXPECT_CALL(webtrans_, GetStreamById).Times(0);
   publisher_->OnGroupAbandoned(1);
   EXPECT_CALL(*track_publisher_, GetCachedObject).Times(0);
@@ -448,7 +603,7 @@
 TEST_F(LivePublisherTest, OnGroupAbandonedTooFarBehind) {
   // Set the delivery timeout to infinite so that TooFarBehind is possible.
   parameters_.delivery_timeout = quic::QuicTimeDelta::Infinite();
-  publisher_->Update(parameters_);
+  publisher_->Update(parameters_, true);
   CreateStream(Location(5, 0), 0, 128);
   struct MoqtPublishDone expected_publish_done = {
       /*request_id=*/kRequestId,
@@ -639,7 +794,11 @@
   // No entries in the stream map.
   EXPECT_CALL(webtrans_, GetStreamById).Times(0);
   parameters_.subscriber_priority = 20;
-  publisher_->Update(parameters_);
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk()), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(parameters_, false);
 }
 
 TEST_F(LivePublisherTest, OnObjectSentTwice) {
@@ -689,7 +848,11 @@
   // Track gets to Group 5.
   CreateStream(Location(5, 0), 0, 128);
   parameters_.subscription_filter = SubscriptionFilter(Location(0, 0), 4);
-  publisher_->Update(parameters_);
+  EXPECT_CALL(mock_bidi_stream_, CanWrite()).WillRepeatedly(Return(true));
+  EXPECT_CALL(mock_bidi_stream_,
+              Writev(SerializedControlMessage(MoqtRequestOk()), _))
+      .WillOnce(Return(absl::OkStatus()));
+  publisher_->Update(parameters_, false);
   EXPECT_CALL(*track_publisher_, GetCachedObject).Times(0);
   publisher_->OnNewObjectAvailable(Location(5, 1), 0, 128);
 }
@@ -717,7 +880,7 @@
 
 TEST_F(LivePublisherTest, OnSubgroupAbandonedOutsideWindow) {
   parameters_.subscription_filter = SubscriptionFilter(Location(20, 0));
-  publisher_->Update(parameters_);
+  publisher_->Update(parameters_, true);
   EXPECT_CALL(mock_uni_stream_, ResetWithUserCode).Times(0);
   publisher_->OnSubgroupAbandoned(1, 0, 1234);
 }
diff --git a/quiche/quic/moqt/moqt_publish_stream.cc b/quiche/quic/moqt/moqt_publish_stream.cc
index d85dea5..261d8f0 100644
--- a/quiche/quic/moqt/moqt_publish_stream.cc
+++ b/quiche/quic/moqt/moqt_publish_stream.cc
@@ -75,28 +75,39 @@
         absl::InvalidArgumentError("REQUEST_OK received with properties"));
     return absl::OkStatus();
   }
-  std::move(response_callback_)(message.parameters);
-  publisher_->Update(message.parameters);
-  return absl::OkStatus();
+  if (response_callback_ != nullptr) {
+    // PUBLISH_OK
+    publisher_->Update(message.parameters, /*from_request_ok=*/true);
+    MoqtResponseCallback callback = std::move(response_callback_);
+    response_callback_ = nullptr;
+    std::move(callback)(message.parameters);
+    return absl::OkStatus();
+  }
+  // REQUEST_UPDATE_OK
+  QUICHE_ASSIGN_OR_RETURN(MessageParameters parameters,
+                          request_update_queue().NextParameters());
+  // Apply the pending parameters to the subscription.
+  publisher_->Update(parameters, /*from_request_ok=*/true);
+  if (publisher_ != nullptr) {
+    // Apply any parameters from the REQUEST_OK.
+    publisher_->Update(message.parameters, /*from_request_ok=*/true);
+  }
+  return request_update_queue().OnControlMessage(message);
 }
 
 absl::Status MoqtPublishRequestStream::OnControlMessage(
     const MoqtRequestError& message) {
-  std::move(response_callback_)(message);
-  return absl::OkStatus();
+  if (response_callback_ != nullptr) {
+    std::move(response_callback_)(message);
+    return absl::OkStatus();
+  }
+  return request_update_queue().OnControlMessage(message);
 }
 
 absl::Status MoqtPublishRequestStream::OnControlMessage(
     const MoqtRequestUpdate& message) {
   QUICHE_RETURN_IF_ERROR(validate_request_id_(message.request_id));
-  MessageParameters in_parameters = message.parameters, out_parameters;
-  out_parameters.largest_object = publisher_->publisher().largest_location();
-  if (in_parameters.subscription_filter.has_value()) {
-    in_parameters.subscription_filter->OnLargestObject(
-        out_parameters.largest_object);
-  }
-  publisher_->Update(in_parameters);
-  CheckStatus(SendRequestOk(MessageParameters()));
+  publisher_->Update(message.parameters, /*from_request_ok=*/false);
   return absl::OkStatus();
 }
 
diff --git a/quiche/quic/moqt/moqt_publish_stream_test.cc b/quiche/quic/moqt/moqt_publish_stream_test.cc
index d339823..a9552c7 100644
--- a/quiche/quic/moqt/moqt_publish_stream_test.cc
+++ b/quiche/quic/moqt/moqt_publish_stream_test.cc
@@ -202,10 +202,9 @@
             request_update.parameters.subscriber_priority);
 
   // Verify filter was updated based on largest location (1, 2) -> (1, 3)
-  // AbsoluteStart
   ASSERT_TRUE(pub_params.subscription_filter.has_value());
   EXPECT_EQ(pub_params.subscription_filter->type(),
-            MoqtFilterType::kAbsoluteStart);
+            MoqtFilterType::kLargestObject);
   EXPECT_EQ(pub_params.subscription_filter->start(), Location(1, 3));
 }
 
diff --git a/quiche/quic/moqt/moqt_session.h b/quiche/quic/moqt/moqt_session.h
index 330b6f7..f823cbc 100644
--- a/quiche/quic/moqt/moqt_session.h
+++ b/quiche/quic/moqt/moqt_session.h
@@ -96,6 +96,10 @@
   bool Subscribe(const FullTrackName& name,
                  SubscribeVisitor* absl_nonnull visitor,
                  const MessageParameters& parameters) override;
+  // TODO(martinduke): There is an edge case where the update moves the filter
+  // window to the right, and the new LARGEST_OBJECT indicates that there's a
+  // gap of objects that has be fetched. This requires a standalone FETCH that
+  // probably should have its objects reported via the SubscribeVisitor.
   bool SubscribeUpdate(const FullTrackName& name,
                        const MessageParameters& parameters,
                        MoqtResponseCallback response_callback) override;
diff --git a/quiche/quic/moqt/moqt_session_test.cc b/quiche/quic/moqt/moqt_session_test.cc
index 24da4bf..c981d33 100644
--- a/quiche/quic/moqt/moqt_session_test.cc
+++ b/quiche/quic/moqt/moqt_session_test.cc
@@ -29,6 +29,7 @@
 #include "quiche/quic/moqt/moqt_framer.h"
 #include "quiche/quic/moqt/moqt_key_value_pair.h"
 #include "quiche/quic/moqt/moqt_known_track_publisher.h"
+#include "quiche/quic/moqt/moqt_live_publisher.h"
 #include "quiche/quic/moqt/moqt_messages.h"
 #include "quiche/quic/moqt/moqt_names.h"
 #include "quiche/quic/moqt/moqt_object.h"
@@ -1903,6 +1904,46 @@
   fetch_wrapper->ReceiveMessage(fetch);
 }
 
+TEST_F(MoqtSessionTest, IncomingJoiningFetchAfterRequestUpdate) {
+  MoqtSubscribe subscribe = DefaultSubscribe();
+  subscribe.parameters.set_forward(false);
+  bidi_wrapper_ = std::make_unique<MoqtBidiStreamTestWrapper>(
+      ResponseStream(kSubscribeByte));
+  MockTrackPublisher* track = CreateTrackPublisher();
+  SetLargestId(track, Location(2, 10));
+  MoqtObjectListener* listener =
+      ReceiveSubscribeSynchronousOk(track, subscribe, bidi_wrapper_.get());
+  LivePublisher* subscription = absl::down_cast<LivePublisher*>(listener);
+  ASSERT_NE(subscription, nullptr);
+
+  SetLargestId(track, Location(4, 10));
+  MessageParameters update_parameters;
+  update_parameters.set_forward(true);
+  update_parameters.subscription_filter.emplace(MoqtFilterType::kLargestObject);
+  MessageParameters expected_ok_parameters;
+  expected_ok_parameters.largest_object = Location(4, 10);
+  EXPECT_CALL(
+      mock_bidi_stream_,
+      Writev(SerializedControlMessage(MoqtRequestOk{expected_ok_parameters}),
+             _));
+  bidi_wrapper_->ReceiveMessage(
+      MoqtRequestUpdate{3, subscribe.request_id, update_parameters});
+  ASSERT_TRUE(subscription->parameters().subscription_filter.has_value());
+  EXPECT_EQ(subscription->parameters().subscription_filter->start(),
+            Location(4, 11));
+
+  webtransport::test::MockStream fetch_stream;
+  std::unique_ptr<MoqtBidiStreamTestWrapper> fetch_wrapper =
+      std::make_unique<MoqtBidiStreamTestWrapper>(
+          ResponseStream(kFetchByte, &fetch_stream));
+  MoqtFetch fetch = DefaultFetch();
+  fetch.request_id = 5;
+  fetch.fetch = JoiningFetchRelative(subscribe.request_id, 2);
+  EXPECT_CALL(*track, StandaloneFetch(Location(2, 0), Location(4, 10), _, _))
+      .WillOnce(Return(std::make_unique<MockFetchTask>()));
+  fetch_wrapper->ReceiveMessage(fetch);
+}
+
 TEST_F(MoqtSessionTest, SendJoiningFetch) {
   webtransport::test::MockStream subscribe_stream;
   std::unique_ptr<MoqtBidiStreamTestWrapper> subscribe_wrapper;
diff --git a/quiche/quic/moqt/moqt_subscribe_stream.cc b/quiche/quic/moqt/moqt_subscribe_stream.cc
index cddee8c..ad69cd5 100644
--- a/quiche/quic/moqt/moqt_subscribe_stream.cc
+++ b/quiche/quic/moqt/moqt_subscribe_stream.cc
@@ -245,8 +245,13 @@
     return SendRequestError(RequestErrorCode::kInternalError, std::nullopt,
                             "no subscription");
   }
-  subscription_->Update(message.parameters);
-  return SendRequestOk(MessageParameters());
+  if (!subscription_->established() || IncomingUpdatesQueued()) {
+    // There are requests waiting in the queue. Buffer this one.
+    QueueIncomingUpdate(message.parameters);
+    return absl::OkStatus();
+  }
+  subscription_->Update(message.parameters, /*from_request_ok=*/false);
+  return absl::OkStatus();
 }
 
 void MoqtSubscribeResponseStream::Detach() {
diff --git a/quiche/quic/moqt/moqt_subscribe_stream_test.cc b/quiche/quic/moqt/moqt_subscribe_stream_test.cc
index ceb6200..499bc47 100644
--- a/quiche/quic/moqt/moqt_subscribe_stream_test.cc
+++ b/quiche/quic/moqt/moqt_subscribe_stream_test.cc
@@ -23,6 +23,7 @@
 #include "quiche/quic/moqt/moqt_names.h"
 #include "quiche/quic/moqt/moqt_object_subscriber.h"
 #include "quiche/quic/moqt/moqt_parser.h"
+#include "quiche/quic/moqt/moqt_publisher.h"
 #include "quiche/quic/moqt/moqt_session_callbacks.h"
 #include "quiche/quic/moqt/moqt_session_interface.h"
 #include "quiche/quic/moqt/moqt_trace_recorder.h"
@@ -351,6 +352,52 @@
   EXPECT_CALL(mock_remove_callback_, Call(_));
 }
 
+TEST_F(MoqtSubscribeResponseStreamTest,
+       ReceiveRequestUpdateBeforeSubscribeAccepted) {
+  auto mock_track_publisher = std::make_shared<MockTrackPublisher>(kTrackName);
+  EXPECT_CALL(visitor_, session).WillRepeatedly(Return(&webtrans_));
+  EXPECT_CALL(visitor_, GetTrackPublisher(kTrackName))
+      .WillOnce(Return(mock_track_publisher));
+  EXPECT_CALL(mock_add_callback_, Call(testing::NotNull()))
+      .WillOnce(Return(true));
+  MoqtObjectListener* captured_listener = nullptr;
+  EXPECT_CALL(*mock_track_publisher, AddObjectListener)
+      .WillOnce([&](MoqtObjectListener* listener, const MessageParameters&) {
+        captured_listener = listener;
+      });
+
+  MoqtSubscribe subscribe;
+  subscribe.request_id = kRequestId;
+  subscribe.full_track_name = kTrackName;
+  QUICHE_EXPECT_OK(stream_->OnControlMessage(subscribe));
+  ASSERT_NE(captured_listener, nullptr);
+
+  // REQUEST_UPDATE arrives before OnSubscribeAccepted(); it is queued and
+  // does not write REQUEST_OK yet.
+  EXPECT_CALL(mock_stream_, Writev).Times(0);
+  MoqtRequestUpdate update;
+  update.request_id = kRequestId + 2;
+  update.existing_request_id = kRequestId;
+  update.parameters.subscriber_priority = 10;
+  QUICHE_EXPECT_OK(stream_->OnControlMessage(update));
+
+  // Accepting the subscription sends SUBSCRIBE_OK followed by REQUEST_OK.
+  {
+    testing::InSequence seq;
+    EXPECT_CALL(mock_stream_,
+                Writev(ControlMessageOfType(MoqtMessageType::kSubscribeOk), _))
+        .WillOnce(Return(absl::OkStatus()));
+    EXPECT_CALL(mock_stream_,
+                Writev(ControlMessageOfType(MoqtMessageType::kRequestOk), _))
+        .WillOnce(Return(absl::OkStatus()));
+  }
+  captured_listener->OnSubscribeAccepted();
+
+  // Test cleanup.
+  EXPECT_CALL(*mock_track_publisher, RemoveObjectListener(captured_listener));
+  EXPECT_CALL(mock_remove_callback_, Call);
+}
+
 TEST_F(MoqtSubscribeResponseStreamTest, ReceiveObjectAck) {
   EXPECT_CALL(visitor_, session).WillRepeatedly(Return(&webtrans_));
   EXPECT_CALL(visitor_, GetTrackPublisher(kTrackName))