Add QUICHE support for the HTTP/3 ORIGIN frame.

The ORIGIN HTTP/3 frame allows a server to indicate what origin or origins
RFC6454] the server would like the client to consider as one or more
members of the Origin Set (Section 2.3 of [ORIGIN]) for the connection
within which it occurs
https://www.rfc-editor.org/rfc/rfc9412.html

Protected by FLAGS_gfe2_reloadable_flag_enable_h3_origin_frame.

PiperOrigin-RevId: 657272516
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index 831d27d..bb0a6da 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -8,6 +8,7 @@
 
 #if defined(QUICHE_FLAG)
 
+QUICHE_FLAG(bool, quiche_reloadable_flag_enable_h3_origin_frame, false, false, "If true, enables support for parsing HTTP/3 ORIGIN frames.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_http2_add_hpack_overhead_bytes2, false, true, "If true, HTTP/2 HEADERS frames will use two additional bytes of HPACK overhead per header in their SpdyHeadersIR::size() estimate. This flag is latched in SpdyHeadersIR to ensure a consistent size() value for a const SpdyHeadersIR regardless of flag state.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_act_upon_invalid_header, true, true, "If true, reject or send error response code upon receiving invalid request or response headers.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_add_stream_info_to_idle_close_detail, false, true, "If true, include stream information in idle timeout connection close detail.")
diff --git a/quiche/quic/core/http/http_decoder.cc b/quiche/quic/core/http/http_decoder.cc
index 00de94f..a08a00e 100644
--- a/quiche/quic/core/http/http_decoder.cc
+++ b/quiche/quic/core/http/http_decoder.cc
@@ -46,7 +46,8 @@
       current_type_field_length_(0),
       remaining_type_field_length_(0),
       error_(QUIC_NO_ERROR),
-      error_detail_("") {
+      error_detail_(""),
+      enable_origin_frame_(GetQuicReloadableFlag(enable_h3_origin_frame)) {
   QUICHE_DCHECK(visitor_);
 }
 
@@ -289,6 +290,12 @@
           visitor_->OnMetadataFrameStart(header_length, current_frame_length_);
       break;
     default:
+      if (enable_origin_frame_ &&
+          current_frame_type_ == static_cast<uint64_t>(HttpFrameType::ORIGIN)) {
+        QUIC_CODE_COUNT_N(enable_h3_origin_frame, 1, 2);
+        continue_processing = visitor_->OnOriginFrameStart(header_length);
+        break;
+      }
       continue_processing = visitor_->OnUnknownFrameStart(
           current_frame_type_, header_length, current_frame_length_);
       break;
@@ -316,6 +323,12 @@
       return true;
     case static_cast<uint64_t>(HttpFrameType::PRIORITY_UPDATE_REQUEST_STREAM):
       return true;
+    case static_cast<uint64_t>(HttpFrameType::ORIGIN):
+      if (enable_origin_frame_) {
+        QUIC_CODE_COUNT_N(enable_h3_origin_frame, 2, 2);
+        return true;
+      }
+      return false;
     case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH):
       return true;
   }
@@ -394,6 +407,11 @@
       break;
     }
     default: {
+      if (enable_origin_frame_ &&
+          current_frame_type_ == static_cast<uint64_t>(HttpFrameType::ORIGIN)) {
+        QUICHE_NOTREACHED();
+        break;
+      }
       continue_processing = HandleUnknownFramePayload(reader);
       break;
     }
@@ -454,6 +472,11 @@
       break;
     }
     default:
+      if (enable_origin_frame_ &&
+          current_frame_type_ == static_cast<uint64_t>(HttpFrameType::ORIGIN)) {
+        QUICHE_NOTREACHED();
+        break;
+      }
       continue_processing = visitor_->OnUnknownFrameEnd();
   }
 
@@ -569,6 +592,13 @@
       }
       return visitor_->OnPriorityUpdateFrame(frame);
     }
+    case static_cast<uint64_t>(HttpFrameType::ORIGIN): {
+      OriginFrame frame;
+      if (!ParseOriginFrame(reader, frame)) {
+        return false;
+      }
+      return visitor_->OnOriginFrame(frame);
+    }
     case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH): {
       AcceptChFrame frame;
       if (!ParseAcceptChFrame(reader, frame)) {
@@ -649,6 +679,19 @@
   return true;
 }
 
+bool HttpDecoder::ParseOriginFrame(QuicDataReader& reader, OriginFrame& frame) {
+  QUICHE_DCHECK(enable_origin_frame_);
+  while (!reader.IsDoneReading()) {
+    absl::string_view origin;
+    if (!reader.ReadStringPiece16(&origin)) {
+      RaiseError(QUIC_HTTP_FRAME_ERROR, "Unable to read ORIGIN origin.");
+      return false;
+    }
+    frame.origins.push_back(std::string(origin));
+  }
+  return true;
+}
+
 bool HttpDecoder::ParseAcceptChFrame(QuicDataReader& reader,
                                      AcceptChFrame& frame) {
   absl::string_view origin;
@@ -683,6 +726,8 @@
       return kPayloadLengthLimit;
     case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH):
       return kPayloadLengthLimit;
+    case static_cast<uint64_t>(HttpFrameType::ORIGIN):
+      return kPayloadLengthLimit;
     default:
       QUICHE_NOTREACHED();
       return 0;
diff --git a/quiche/quic/core/http/http_decoder.h b/quiche/quic/core/http/http_decoder.h
index e959807..e928cca 100644
--- a/quiche/quic/core/http/http_decoder.h
+++ b/quiche/quic/core/http/http_decoder.h
@@ -83,6 +83,13 @@
     // Called when a PRIORITY_UPDATE frame has been successfully parsed.
     virtual bool OnPriorityUpdateFrame(const PriorityUpdateFrame& frame) = 0;
 
+    // Called when an ORIGIN frame has been received.
+    // |header_length| contains ORIGIN frame length and payload length.
+    virtual bool OnOriginFrameStart(QuicByteCount header_length) = 0;
+
+    // Called when an ORIGIN frame has been successfully parsed.
+    virtual bool OnOriginFrame(const OriginFrame& frame) = 0;
+
     // Called when an ACCEPT_CH frame has been received.
     // |header_length| contains ACCEPT_CH frame length and payload length.
     virtual bool OnAcceptChFrameStart(QuicByteCount header_length) = 0;
@@ -247,6 +254,9 @@
   bool ParsePriorityUpdateFrame(QuicDataReader& reader,
                                 PriorityUpdateFrame& frame);
 
+  // Parses the payload of an ORIGIN frame from |reader| into |frame|.
+  bool ParseOriginFrame(QuicDataReader& reader, OriginFrame& frame);
+
   // Parses the payload of an ACCEPT_CH frame from |reader| into |frame|.
   bool ParseAcceptChFrame(QuicDataReader& reader, AcceptChFrame& frame);
 
@@ -283,6 +293,8 @@
   std::array<char, sizeof(uint64_t)> length_buffer_;
   // Remaining unparsed type field data.
   std::array<char, sizeof(uint64_t)> type_buffer_;
+  // Latched value of reloadable flag enable_h3_origin_frame.
+  bool enable_origin_frame_;
 };
 
 }  // namespace quic
diff --git a/quiche/quic/core/http/http_decoder_test.cc b/quiche/quic/core/http/http_decoder_test.cc
index b7f3255..72fdf1f 100644
--- a/quiche/quic/core/http/http_decoder_test.cc
+++ b/quiche/quic/core/http/http_decoder_test.cc
@@ -57,6 +57,8 @@
     ON_CALL(visitor_, OnPriorityUpdateFrame(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnAcceptChFrameStart(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnAcceptChFrame(_)).WillByDefault(Return(true));
+    ON_CALL(visitor_, OnOriginFrameStart(_)).WillByDefault(Return(true));
+    ON_CALL(visitor_, OnOriginFrame(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnMetadataFrameStart(_, _)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnMetadataFramePayload(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnMetadataFrameEnd()).WillByDefault(Return(true));
@@ -1095,6 +1097,119 @@
   EXPECT_EQ("", decoder_.error_detail());
 }
 
+TEST_F(HttpDecoderTest, OriginFrame) {
+  if (!GetQuicReloadableFlag(enable_h3_origin_frame)) {
+    return;
+  }
+  InSequence s;
+  std::string input1;
+  ASSERT_TRUE(
+      absl::HexStringToBytes("0C"   // type (ORIGIN)
+                             "00",  // length
+                             &input1));
+
+  OriginFrame origin1;
+
+  // Visitor pauses processing.
+  EXPECT_CALL(visitor_, OnOriginFrameStart(2)).WillOnce(Return(false));
+  absl::string_view remaining_input(input1);
+  QuicByteCount processed_bytes =
+      ProcessInputWithGarbageAppended(remaining_input);
+  EXPECT_EQ(2u, processed_bytes);
+  remaining_input = remaining_input.substr(processed_bytes);
+
+  EXPECT_CALL(visitor_, OnOriginFrame(origin1)).WillOnce(Return(false));
+  processed_bytes = ProcessInputWithGarbageAppended(remaining_input);
+  EXPECT_EQ(remaining_input.size(), processed_bytes);
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  // Process the full frame.
+  EXPECT_CALL(visitor_, OnOriginFrameStart(2));
+  EXPECT_CALL(visitor_, OnOriginFrame(origin1));
+  EXPECT_EQ(input1.size(), ProcessInput(input1));
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  // Process the frame incrementally.
+  EXPECT_CALL(visitor_, OnOriginFrameStart(2));
+  EXPECT_CALL(visitor_, OnOriginFrame(origin1));
+  ProcessInputCharByChar(input1);
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  std::string input2;
+  ASSERT_TRUE(
+      absl::HexStringToBytes("0C"       // type (ORIGIN)
+                             "0A"       // length
+                             "0003"     // length of origin
+                             "666f6f"   // origin "foo"
+                             "0003"     // length of origin
+                             "626172",  // origin "bar"
+                             &input2));
+  ASSERT_EQ(12, input2.length());
+
+  OriginFrame origin2;
+  origin2.origins = {"foo", "bar"};
+
+  // Visitor pauses processing.
+  EXPECT_CALL(visitor_, OnOriginFrameStart(2)).WillOnce(Return(false));
+  remaining_input = input2;
+  processed_bytes = ProcessInputWithGarbageAppended(remaining_input);
+  EXPECT_EQ(2u, processed_bytes);
+  remaining_input = remaining_input.substr(processed_bytes);
+
+  EXPECT_CALL(visitor_, OnOriginFrame(origin2)).WillOnce(Return(false));
+  processed_bytes = ProcessInputWithGarbageAppended(remaining_input);
+  EXPECT_EQ(remaining_input.size(), processed_bytes);
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  // Process the full frame.
+  EXPECT_CALL(visitor_, OnOriginFrameStart(2));
+  EXPECT_CALL(visitor_, OnOriginFrame(origin2));
+  EXPECT_EQ(input2.size(), ProcessInput(input2));
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+
+  // Process the frame incrementally.
+  EXPECT_CALL(visitor_, OnOriginFrameStart(2));
+  EXPECT_CALL(visitor_, OnOriginFrame(origin2));
+  ProcessInputCharByChar(input2);
+  EXPECT_THAT(decoder_.error(), IsQuicNoError());
+  EXPECT_EQ("", decoder_.error_detail());
+}
+
+TEST_F(HttpDecoderTest, OriginFrameDisabled) {
+  if (GetQuicReloadableFlag(enable_h3_origin_frame)) {
+    return;
+  }
+  InSequence s;
+
+  std::string input1;
+  ASSERT_TRUE(
+      absl::HexStringToBytes("0C"   // type (ORIGIN)
+                             "00",  // length
+                             &input1));
+  EXPECT_CALL(visitor_, OnUnknownFrameStart(0x0C, 2, 0));
+  EXPECT_CALL(visitor_, OnUnknownFrameEnd());
+  EXPECT_EQ(ProcessInput(input1), input1.size());
+
+  std::string input2;
+  ASSERT_TRUE(
+      absl::HexStringToBytes("0C"       // type (ORIGIN)
+                             "0A"       // length
+                             "0003"     // length of origin
+                             "666f6f"   // origin "foo"
+                             "0003"     // length of origin
+                             "626172",  // origin "bar"
+                             &input2));
+  EXPECT_CALL(visitor_, OnUnknownFrameStart(0x0C, 2, input2.size() - 2));
+  EXPECT_CALL(visitor_, OnUnknownFramePayload(input2.substr(2)));
+  EXPECT_CALL(visitor_, OnUnknownFrameEnd());
+  EXPECT_EQ(ProcessInput(input2), input2.size());
+}
+
 TEST_F(HttpDecoderTest, WebTransportStreamDisabled) {
   InSequence s;
 
diff --git a/quiche/quic/core/http/http_encoder.cc b/quiche/quic/core/http/http_encoder.cc
index 8ea61b9..3edb43a 100644
--- a/quiche/quic/core/http/http_encoder.cc
+++ b/quiche/quic/core/http/http_encoder.cc
@@ -196,6 +196,36 @@
   return frame;
 }
 
+std::string HttpEncoder::SerializeOriginFrame(const OriginFrame& origin) {
+  QuicByteCount payload_length = 0;
+  for (const std::string& entry : origin.origins) {
+    constexpr QuicByteCount kLengthFieldOverhead = 2;
+    payload_length += kLengthFieldOverhead + entry.size();
+  }
+
+  QuicByteCount total_length =
+      GetTotalLength(payload_length, HttpFrameType::ORIGIN);
+
+  std::string frame;
+  frame.resize(total_length);
+  QuicDataWriter writer(total_length, frame.data());
+
+  if (!WriteFrameHeader(payload_length, HttpFrameType::ORIGIN, &writer)) {
+    QUIC_DLOG(ERROR) << "Http encoder failed to serialize ORIGIN frame header.";
+    return {};
+  }
+
+  for (const std::string& entry : origin.origins) {
+    if (!writer.WriteStringPiece16(entry)) {
+      QUIC_DLOG(ERROR)
+          << "Http encoder failed to serialize ACCEPT_CH frame payload.";
+      return {};
+    }
+  }
+
+  return frame;
+}
+
 std::string HttpEncoder::SerializeGreasingFrame() {
   uint64_t frame_type;
   QuicByteCount payload_length;
diff --git a/quiche/quic/core/http/http_encoder.h b/quiche/quic/core/http/http_encoder.h
index d0d8408..e00be7d 100644
--- a/quiche/quic/core/http/http_encoder.h
+++ b/quiche/quic/core/http/http_encoder.h
@@ -47,6 +47,9 @@
   // Serializes an ACCEPT_CH frame.
   static std::string SerializeAcceptChFrame(const AcceptChFrame& accept_ch);
 
+  // Serializes an ORIGIN frame.
+  static std::string SerializeOriginFrame(const OriginFrame& origin);
+
   // Serializes a frame with reserved frame type specified in
   // https://tools.ietf.org/html/draft-ietf-quic-http-25#section-7.2.9.
   static std::string SerializeGreasingFrame();
diff --git a/quiche/quic/core/http/http_encoder_test.cc b/quiche/quic/core/http/http_encoder_test.cc
index e252fc5..958e3f9 100644
--- a/quiche/quic/core/http/http_encoder_test.cc
+++ b/quiche/quic/core/http/http_encoder_test.cc
@@ -94,6 +94,33 @@
       reinterpret_cast<char*>(output2), ABSL_ARRAYSIZE(output2));
 }
 
+TEST(HttpEncoderTest, SerializeEmptyOriginFrame) {
+  OriginFrame frame;
+  uint8_t expected[] = {0x0C,   // type (ACCEPT_CH)
+                        0x00};  // length
+
+  std::string output = HttpEncoder::SerializeOriginFrame(frame);
+  quiche::test::CompareCharArraysWithHexError(
+      "ORIGIN", output.data(), output.length(),
+      reinterpret_cast<char*>(expected), ABSL_ARRAYSIZE(expected));
+}
+
+TEST(HttpEncoderTest, SerializeOriginFrame) {
+  OriginFrame frame;
+  frame.origins = {"foo", "bar"};
+  uint8_t expected[] = {0x0C,                // type (ORIGIN)
+                        0x0A,                // length
+                        0x00, 0x003,         // length of origin
+                        0x66, 0x6f,  0x6f,   // origin "foo"
+                        0x00, 0x003,         // length of origin
+                        0x62, 0x61,  0x72};  // origin "bar"
+
+  std::string output = HttpEncoder::SerializeOriginFrame(frame);
+  quiche::test::CompareCharArraysWithHexError(
+      "ORIGIN", output.data(), output.length(),
+      reinterpret_cast<char*>(expected), ABSL_ARRAYSIZE(expected));
+}
+
 TEST(HttpEncoderTest, SerializeAcceptChFrame) {
   AcceptChFrame accept_ch;
   uint8_t output1[] = {0x40, 0x89,  // type (ACCEPT_CH)
diff --git a/quiche/quic/core/http/http_frames.h b/quiche/quic/core/http/http_frames.h
index 5b72bb0..645f5c0 100644
--- a/quiche/quic/core/http/http_frames.h
+++ b/quiche/quic/core/http/http_frames.h
@@ -23,10 +23,12 @@
 enum class HttpFrameType {
   DATA = 0x0,
   HEADERS = 0x1,
-  CANCEL_PUSH = 0X3,
+  CANCEL_PUSH = 0x3,
   SETTINGS = 0x4,
   PUSH_PROMISE = 0x5,
   GOAWAY = 0x7,
+  // https://www.rfc-editor.org/rfc/rfc9412.html
+  ORIGIN = 0xC,
   MAX_PUSH_ID = 0xD,
   // https://tools.ietf.org/html/draft-davidben-http-client-hint-reliability-02
   ACCEPT_CH = 0x89,
@@ -99,6 +101,19 @@
   bool operator==(const GoAwayFrame& rhs) const { return id == rhs.id; }
 };
 
+// https://www.rfc-editor.org/rfc/rfc9412.html
+// The ORIGIN HTTP/3 frame allows a server to indicate what origin or origins
+// [RFC6454] the server would like the client to consider as one or more
+// members of the Origin Set (Section 2.3 of [ORIGIN]) for the connection
+// within which it occurs
+struct QUICHE_EXPORT OriginFrame {
+  std::vector<std::string> origins;
+
+  bool operator==(const OriginFrame& rhs) const {
+    return origins == rhs.origins;
+  }
+};
+
 // https://httpwg.org/http-extensions/draft-ietf-httpbis-priority.html
 //
 // The PRIORITY_UPDATE frame specifies the sender-advised priority of a stream.
diff --git a/quiche/quic/core/http/quic_receive_control_stream.cc b/quiche/quic/core/http/quic_receive_control_stream.cc
index 4cba9d5..08edeb2 100644
--- a/quiche/quic/core/http/quic_receive_control_stream.cc
+++ b/quiche/quic/core/http/quic_receive_control_stream.cc
@@ -150,6 +150,22 @@
   return spdy_session_->OnPriorityUpdateForRequestStream(stream_id, *priority);
 }
 
+bool QuicReceiveControlStream::OnOriginFrameStart(
+    QuicByteCount /* header_length */) {
+  return ValidateFrameType(HttpFrameType::ORIGIN);
+}
+
+bool QuicReceiveControlStream::OnOriginFrame(const OriginFrame& frame) {
+  QUICHE_DCHECK_EQ(Perspective::IS_CLIENT, spdy_session()->perspective());
+
+  if (spdy_session()->debug_visitor()) {
+    spdy_session()->debug_visitor()->OnOriginFrameReceived(frame);
+  }
+
+  spdy_session()->OnOriginFrame(frame);
+  return false;
+}
+
 bool QuicReceiveControlStream::OnAcceptChFrameStart(
     QuicByteCount /* header_length */) {
   return ValidateFrameType(HttpFrameType::ACCEPT_CH);
@@ -217,7 +233,9 @@
       (spdy_session()->perspective() == Perspective::IS_CLIENT &&
        frame_type == HttpFrameType::MAX_PUSH_ID) ||
       (spdy_session()->perspective() == Perspective::IS_SERVER &&
-       frame_type == HttpFrameType::ACCEPT_CH)) {
+       ((GetQuicReloadableFlag(enable_h3_origin_frame) &&
+         frame_type == HttpFrameType::ORIGIN) ||
+        frame_type == HttpFrameType::ACCEPT_CH))) {
     stream_delegate()->OnStreamError(
         QUIC_HTTP_FRAME_UNEXPECTED_ON_CONTROL_STREAM,
         absl::StrCat("Invalid frame type ", static_cast<int>(frame_type),
diff --git a/quiche/quic/core/http/quic_receive_control_stream.h b/quiche/quic/core/http/quic_receive_control_stream.h
index c7f4ee2..b1cf610 100644
--- a/quiche/quic/core/http/quic_receive_control_stream.h
+++ b/quiche/quic/core/http/quic_receive_control_stream.h
@@ -48,6 +48,8 @@
   bool OnHeadersFrameEnd() override;
   bool OnPriorityUpdateFrameStart(QuicByteCount header_length) override;
   bool OnPriorityUpdateFrame(const PriorityUpdateFrame& frame) override;
+  bool OnOriginFrameStart(QuicByteCount header_length) override;
+  bool OnOriginFrame(const OriginFrame& frame) override;
   bool OnAcceptChFrameStart(QuicByteCount header_length) override;
   bool OnAcceptChFrame(const AcceptChFrame& frame) override;
   void OnWebTransportStreamFrameType(QuicByteCount header_length,
diff --git a/quiche/quic/core/http/quic_receive_control_stream_test.cc b/quiche/quic/core/http/quic_receive_control_stream_test.cc
index 79773c0..b3c0cc3 100644
--- a/quiche/quic/core/http/quic_receive_control_stream_test.cc
+++ b/quiche/quic/core/http/quic_receive_control_stream_test.cc
@@ -455,6 +455,51 @@
       QuicStreamFrame(id, /* fin = */ false, offset, accept_ch_frame));
 }
 
+TEST_P(QuicReceiveControlStreamTest, ReceiveOriginFrame) {
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session_.set_debug_visitor(&debug_visitor);
+
+  const QuicStreamId id = receive_control_stream_->id();
+  QuicStreamOffset offset = 1;
+
+  // Receive SETTINGS frame.
+  SettingsFrame settings;
+  std::string settings_frame = HttpEncoder::SerializeSettingsFrame(settings);
+  EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(settings));
+  receive_control_stream_->OnStreamFrame(
+      QuicStreamFrame(id, /* fin = */ false, offset, settings_frame));
+  offset += settings_frame.length();
+
+  // Receive ORIGIN frame.
+  std::string origin_frame;
+  ASSERT_TRUE(
+      absl::HexStringToBytes("0C"   // type (ORIGIN)
+                             "00",  // length
+                             &origin_frame));
+
+  if (GetQuicReloadableFlag(enable_h3_origin_frame)) {
+    if (perspective() == Perspective::IS_CLIENT) {
+      EXPECT_CALL(debug_visitor, OnOriginFrameReceived(_));
+    } else {
+      EXPECT_CALL(*connection_,
+                  CloseConnection(
+                      QUIC_HTTP_FRAME_UNEXPECTED_ON_CONTROL_STREAM,
+                      "Invalid frame type 12 received on control stream.", _))
+          .WillOnce(
+              Invoke(connection_, &MockQuicConnection::ReallyCloseConnection));
+      EXPECT_CALL(*connection_, SendConnectionClosePacket(_, _, _));
+      EXPECT_CALL(session_, OnConnectionClosed(_, _));
+    }
+  } else {
+    EXPECT_CALL(debug_visitor,
+                OnUnknownFrameReceived(id, /* frame_type = */ 0x0c,
+                                       /* payload_length = */ 0));
+  }
+
+  receive_control_stream_->OnStreamFrame(
+      QuicStreamFrame(id, /* fin = */ false, offset, origin_frame));
+}
+
 TEST_P(QuicReceiveControlStreamTest, UnknownFrameBeforeSettings) {
   std::string unknown_frame;
   ASSERT_TRUE(
diff --git a/quiche/quic/core/http/quic_spdy_session.cc b/quiche/quic/core/http/quic_spdy_session.cc
index edfbb4a..e946627 100644
--- a/quiche/quic/core/http/quic_spdy_session.cc
+++ b/quiche/quic/core/http/quic_spdy_session.cc
@@ -143,6 +143,11 @@
     session_->OnAcceptChFrameReceivedViaAlps(frame);
     return true;
   }
+  bool OnOriginFrameStart(QuicByteCount /*header_length*/) override {
+    QUICHE_NOTREACHED();
+    return true;
+  }
+  bool OnOriginFrame(const OriginFrame& /*frame*/) override { return true; }
   void OnWebTransportStreamFrameType(
       QuicByteCount /*header_length*/,
       WebTransportSessionId /*session_id*/) override {
diff --git a/quiche/quic/core/http/quic_spdy_session.h b/quiche/quic/core/http/quic_spdy_session.h
index 92cf9e7..3c2c11b 100644
--- a/quiche/quic/core/http/quic_spdy_session.h
+++ b/quiche/quic/core/http/quic_spdy_session.h
@@ -82,6 +82,7 @@
   virtual void OnGoAwayFrameReceived(const GoAwayFrame& /*frame*/) = 0;
   virtual void OnPriorityUpdateFrameReceived(
       const PriorityUpdateFrame& /*frame*/) = 0;
+  virtual void OnOriginFrameReceived(const OriginFrame& /*frame*/) {}
   virtual void OnAcceptChFrameReceived(const AcceptChFrame& /*frame*/) {}
 
   // Incoming HTTP/3 frames on request or push streams.
@@ -197,6 +198,10 @@
   bool OnPriorityUpdateForRequestStream(QuicStreamId stream_id,
                                         HttpStreamPriority priority);
 
+  // Called when an HTTP/3 ORIGIN frame has been received.
+  // This method will only be called for client sessions.
+  virtual void OnOriginFrame(const OriginFrame& /*frame*/) {}
+
   // Called when an HTTP/3 ACCEPT_CH frame has been received.
   // This method will only be called for client sessions.
   virtual void OnAcceptChFrame(const AcceptChFrame& /*frame*/) {}
diff --git a/quiche/quic/core/http/quic_spdy_stream.cc b/quiche/quic/core/http/quic_spdy_stream.cc
index 59b0644..933cd50 100644
--- a/quiche/quic/core/http/quic_spdy_stream.cc
+++ b/quiche/quic/core/http/quic_spdy_stream.cc
@@ -129,6 +129,16 @@
     return false;
   }
 
+  bool OnOriginFrameStart(QuicByteCount /*header_length*/) override {
+    CloseConnectionOnWrongFrame("ORIGIN");
+    return false;
+  }
+
+  bool OnOriginFrame(const OriginFrame& /*frame*/) override {
+    CloseConnectionOnWrongFrame("ORIGIN");
+    return false;
+  }
+
   bool OnAcceptChFrameStart(QuicByteCount /*header_length*/) override {
     CloseConnectionOnWrongFrame("ACCEPT_CH");
     return false;
diff --git a/quiche/quic/test_tools/quic_test_utils.h b/quiche/quic/test_tools/quic_test_utils.h
index 276a2ac..d0d98ab 100644
--- a/quiche/quic/test_tools/quic_test_utils.h
+++ b/quiche/quic/test_tools/quic_test_utils.h
@@ -1028,6 +1028,7 @@
   MOCK_METHOD(void, OnGoAwayFrameReceived, (const GoAwayFrame&), (override));
   MOCK_METHOD(void, OnPriorityUpdateFrameReceived, (const PriorityUpdateFrame&),
               (override));
+  MOCK_METHOD(void, OnOriginFrameReceived, (const OriginFrame&), (override));
   MOCK_METHOD(void, OnAcceptChFrameReceived, (const AcceptChFrame&),
               (override));
 
@@ -1500,6 +1501,9 @@
   MOCK_METHOD(bool, OnPriorityUpdateFrame, (const PriorityUpdateFrame& frame),
               (override));
 
+  MOCK_METHOD(bool, OnOriginFrameStart, (QuicByteCount header_length),
+              (override));
+  MOCK_METHOD(bool, OnOriginFrame, (const OriginFrame& frame), (override));
   MOCK_METHOD(bool, OnAcceptChFrameStart, (QuicByteCount header_length),
               (override));
   MOCK_METHOD(bool, OnAcceptChFrame, (const AcceptChFrame& frame), (override));