First capsule interop attempt
diff --git a/quic/core/http/http_constants.cc b/quic/core/http/http_constants.cc
index f3ed523..2c0ec24 100644
--- a/quic/core/http/http_constants.cc
+++ b/quic/core/http/http_constants.cc
@@ -17,7 +17,8 @@
     RETURN_STRING_LITERAL(SETTINGS_QPACK_MAX_TABLE_CAPACITY);
     RETURN_STRING_LITERAL(SETTINGS_MAX_FIELD_SECTION_SIZE);
     RETURN_STRING_LITERAL(SETTINGS_QPACK_BLOCKED_STREAMS);
-    RETURN_STRING_LITERAL(SETTINGS_H3_DATAGRAM);
+    RETURN_STRING_LITERAL(SETTINGS_H3_DATAGRAM_DRAFT00);
+    RETURN_STRING_LITERAL(SETTINGS_H3_DATAGRAM_DRAFT03);
     RETURN_STRING_LITERAL(SETTINGS_WEBTRANS_DRAFT00);
   }
   return absl::StrCat("UNSUPPORTED_SETTINGS_TYPE(", identifier, ")");
diff --git a/quic/core/http/http_constants.h b/quic/core/http/http_constants.h
index c13a1a8..94328ac 100644
--- a/quic/core/http/http_constants.h
+++ b/quic/core/http/http_constants.h
@@ -38,8 +38,10 @@
   // Same value as spdy::SETTINGS_MAX_HEADER_LIST_SIZE.
   SETTINGS_MAX_FIELD_SECTION_SIZE = 0x06,
   SETTINGS_QPACK_BLOCKED_STREAMS = 0x07,
-  // draft-ietf-masque-h3-datagram.
-  SETTINGS_H3_DATAGRAM = 0x276,
+  // draft-ietf-masque-h3-datagram-00.
+  SETTINGS_H3_DATAGRAM_DRAFT00 = 0x276,
+  // draft-ietf-masque-h3-datagram-03.
+  SETTINGS_H3_DATAGRAM_DRAFT03 = 0xffd276,
   // draft-ietf-webtrans-http3-00
   SETTINGS_WEBTRANS_DRAFT00 = 0x2b603742,
 };
diff --git a/quic/core/http/http_decoder.cc b/quic/core/http/http_decoder.cc
index b20789b..c22fc32 100644
--- a/quic/core/http/http_decoder.cc
+++ b/quic/core/http/http_decoder.cc
@@ -266,6 +266,8 @@
     case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH):
       continue_processing = visitor_->OnAcceptChFrameStart(header_length);
       break;
+    case static_cast<uint64_t>(HttpFrameType::CAPSULE):
+      break;
     default:
       continue_processing = visitor_->OnUnknownFrameStart(
           current_frame_type_, header_length, current_frame_length_);
@@ -335,6 +337,10 @@
       continue_processing = BufferOrParsePayload(reader);
       break;
     }
+    case static_cast<uint64_t>(HttpFrameType::CAPSULE): {
+      continue_processing = BufferOrParsePayload(reader);
+      break;
+    }
     default: {
       continue_processing = HandleUnknownFramePayload(reader);
       break;
@@ -401,6 +407,12 @@
       continue_processing = BufferOrParsePayload(reader);
       break;
     }
+    case static_cast<uint64_t>(HttpFrameType::CAPSULE): {
+      // If frame payload is not empty, FinishParsing() is skipped.
+      QUICHE_DCHECK_EQ(0u, current_frame_length_);
+      continue_processing = BufferOrParsePayload(reader);
+      break;
+    }
     default:
       continue_processing = visitor_->OnUnknownFrameEnd();
   }
@@ -537,6 +549,13 @@
       }
       return visitor_->OnAcceptChFrame(frame);
     }
+    case static_cast<uint64_t>(HttpFrameType::CAPSULE): {
+      CapsuleFrame frame;
+      if (!ParseCapsuleFrame(reader, &frame)) {
+        return false;
+      }
+      return visitor_->OnCapsuleFrame(frame);
+    }
     default:
       // Only above frame types are parsed by ParseEntirePayload().
       QUICHE_NOTREACHED();
@@ -662,6 +681,59 @@
   return true;
 }
 
+bool HttpDecoder::ParseCapsuleFrame(QuicDataReader* reader,
+                                    CapsuleFrame* frame) {
+  uint64_t capsule_type64;
+  if (!reader->ReadVarInt62(&capsule_type64)) {
+    RaiseError(QUIC_HTTP_FRAME_ERROR, "Unable to parse capsule type");
+    return false;
+  }
+  *frame = CapsuleFrame(static_cast<CapsuleType>(capsule_type64));
+  switch (frame->capsule_type) {
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      if (!reader->ReadVarInt62(
+              &frame->register_datagram_context_capsule.context_id)) {
+        RaiseError(
+            QUIC_HTTP_FRAME_ERROR,
+            "Unable to parse capsule REGISTER_DATAGRAM_CONTEXT context ID");
+        return false;
+      }
+      frame->register_datagram_context_capsule.context_extensions =
+          reader->ReadRemainingPayload();
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      if (!reader->ReadVarInt62(
+              &frame->close_datagram_context_capsule.context_id)) {
+        RaiseError(QUIC_HTTP_FRAME_ERROR,
+                   "Unable to parse capsule CLOSE_DATAGRAM_CONTEXT context ID");
+        return false;
+      }
+      frame->close_datagram_context_capsule.context_extensions =
+          reader->ReadRemainingPayload();
+      break;
+    case CapsuleType::DATAGRAM:
+      if (datagram_context_id_present_) {
+        uint64_t context_id;
+        if (!reader->ReadVarInt62(&context_id)) {
+          RaiseError(QUIC_HTTP_FRAME_ERROR,
+                     "Unable to parse capsule DATAGRAM context ID");
+          return false;
+        }
+        frame->datagram_capsule.context_id = context_id;
+      }
+      frame->datagram_capsule.http_datagram_payload =
+          reader->ReadRemainingPayload();
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      frame->register_datagram_no_context_capsule.context_extensions =
+          reader->ReadRemainingPayload();
+      break;
+    default:
+      frame->unknown_capsule_data = reader->ReadRemainingPayload();
+  }
+  return true;
+}
+
 QuicByteCount HttpDecoder::MaxFrameLength(uint64_t frame_type) {
   switch (frame_type) {
     case static_cast<uint64_t>(HttpFrameType::SETTINGS):
@@ -682,6 +754,9 @@
     case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH):
       // This limit is arbitrary.
       return 1024 * 1024;
+    case static_cast<uint64_t>(HttpFrameType::CAPSULE):
+      // This limit is arbitrary.
+      return 1024 * 1024;
     default:
       // Other frames require no data buffering, so it's safe to have no limit.
       return std::numeric_limits<QuicByteCount>::max();
diff --git a/quic/core/http/http_decoder.h b/quic/core/http/http_decoder.h
index 7cbf205..8d96c84 100644
--- a/quic/core/http/http_decoder.h
+++ b/quic/core/http/http_decoder.h
@@ -94,6 +94,9 @@
     // Called when an ACCEPT_CH frame has been successfully parsed.
     virtual bool OnAcceptChFrame(const AcceptChFrame& frame) = 0;
 
+    // Called when a CAPSULE frame has been successfully parsed.
+    virtual bool OnCapsuleFrame(const CapsuleFrame& frame) = 0;
+
     // Called when a WEBTRANSPORT_STREAM frame type and the session ID varint
     // immediately following it has been received.  Any further parsing should
     // be done by the stream itself, and not the parser. Note that this does not
@@ -148,6 +151,10 @@
   // Returns true if input data processed so far ends on a frame boundary.
   bool AtFrameBoundary() const { return state_ == STATE_READING_FRAME_TYPE; }
 
+  void set_datagram_context_id_present(bool datagram_context_id_present) {
+    datagram_context_id_present_ = datagram_context_id_present;
+  }
+
  private:
   friend test::HttpDecoderPeer;
 
@@ -230,6 +237,9 @@
   // Parses the payload of an ACCEPT_CH frame from |reader| into |frame|.
   bool ParseAcceptChFrame(QuicDataReader* reader, AcceptChFrame* frame);
 
+  // Parses the payload of an CAPSULE frame from |reader| into |frame|.
+  bool ParseCapsuleFrame(QuicDataReader* reader, CapsuleFrame* frame);
+
   // Returns the max frame size of a given |frame_type|.
   QuicByteCount MaxFrameLength(uint64_t frame_type);
 
@@ -237,6 +247,8 @@
   Visitor* const visitor_;  // Unowned.
   // Whether WEBTRANSPORT_STREAM should be parsed.
   bool allow_web_transport_stream_;
+  // Whether HTTP Datagram Context IDs are present.
+  bool datagram_context_id_present_ = false;
   // Current state of the parsing.
   HttpDecoderState state_;
   // Type of the frame currently being parsed.
diff --git a/quic/core/http/http_decoder_test.cc b/quic/core/http/http_decoder_test.cc
index 7fa09ba..6a8491e 100644
--- a/quic/core/http/http_decoder_test.cc
+++ b/quic/core/http/http_decoder_test.cc
@@ -89,6 +89,7 @@
               (QuicByteCount header_length),
               (override));
   MOCK_METHOD(bool, OnAcceptChFrame, (const AcceptChFrame& frame), (override));
+  MOCK_METHOD(bool, OnCapsuleFrame, (const CapsuleFrame& frame), (override));
   MOCK_METHOD(void,
               OnWebTransportStreamFrameType,
               (QuicByteCount header_length, WebTransportSessionId session_id),
@@ -125,6 +126,7 @@
     ON_CALL(visitor_, OnPriorityUpdateFrame(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnAcceptChFrameStart(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnAcceptChFrame(_)).WillByDefault(Return(true));
+    ON_CALL(visitor_, OnCapsuleFrame(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnUnknownFrameStart(_, _, _)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnUnknownFramePayload(_)).WillByDefault(Return(true));
     ON_CALL(visitor_, OnUnknownFrameEnd()).WillByDefault(Return(true));
@@ -1043,6 +1045,88 @@
   EXPECT_EQ("", decoder_.error_detail());
 }
 
+TEST_F(HttpDecoderTest, RegisterDatagramContextCapsuleFrame) {
+  std::string input = absl::HexStringToBytes(
+      "80ffcab5"    // type (CAPSULE)
+      "06"          // length
+      "00"          // capsule type (REGISTER_DATAGRAM_CONTEXT)
+      "08"          // context ID
+      "01020304");  // context extensions
+
+  CapsuleFrame frame(CapsuleType::REGISTER_DATAGRAM_CONTEXT);
+  frame.register_datagram_context_capsule.context_id = 8;
+  std::array<char, 4> context_extensions = {1, 2, 3, 4};
+  frame.register_datagram_context_capsule.context_extensions =
+      absl::string_view(context_extensions.data(), context_extensions.size());
+  EXPECT_CALL(visitor_, OnCapsuleFrame(frame));
+  EXPECT_EQ(ProcessInput(input), input.size());
+}
+
+TEST_F(HttpDecoderTest, CloseDatagramContextCapsuleFrame) {
+  std::string input = absl::HexStringToBytes(
+      "80ffcab5"    // type (CAPSULE)
+      "06"          // length
+      "01"          // capsule type (CLOSE_DATAGRAM_CONTEXT)
+      "08"          // context ID
+      "01020304");  // context extensions
+
+  CapsuleFrame frame(CapsuleType::CLOSE_DATAGRAM_CONTEXT);
+  frame.close_datagram_context_capsule.context_id = 8;
+  std::array<char, 4> context_extensions = {1, 2, 3, 4};
+  frame.close_datagram_context_capsule.context_extensions =
+      absl::string_view(context_extensions.data(), context_extensions.size());
+  EXPECT_CALL(visitor_, OnCapsuleFrame(frame));
+  EXPECT_EQ(ProcessInput(input), input.size());
+}
+
+TEST_F(HttpDecoderTest, DatagramWithoutContextCapsuleFrame) {
+  std::string input = absl::HexStringToBytes(
+      "80ffcab5"    // type (CAPSULE)
+      "05"          // length
+      "02"          // capsule type (DATAGRAM)
+      "01020304");  // context extensions
+
+  CapsuleFrame frame(CapsuleType::DATAGRAM);
+  std::array<char, 4> datagram_payload = {1, 2, 3, 4};
+  frame.datagram_capsule.http_datagram_payload =
+      absl::string_view(datagram_payload.data(), datagram_payload.size());
+  EXPECT_CALL(visitor_, OnCapsuleFrame(frame));
+  EXPECT_EQ(ProcessInput(input), input.size());
+}
+
+TEST_F(HttpDecoderTest, DatagramWithContextCapsuleFrame) {
+  decoder_.set_datagram_context_id_present(true);
+  std::string input = absl::HexStringToBytes(
+      "80ffcab5"    // type (CAPSULE)
+      "06"          // length
+      "02"          // capsule type (DATAGRAM)
+      "08"          // context ID
+      "01020304");  // context extensions
+
+  CapsuleFrame frame(CapsuleType::DATAGRAM);
+  frame.datagram_capsule.context_id = 8;
+  std::array<char, 4> datagram_payload = {1, 2, 3, 4};
+  frame.datagram_capsule.http_datagram_payload =
+      absl::string_view(datagram_payload.data(), datagram_payload.size());
+  EXPECT_CALL(visitor_, OnCapsuleFrame(frame));
+  EXPECT_EQ(ProcessInput(input), input.size());
+}
+
+TEST_F(HttpDecoderTest, RegisterDatagramNoContextCapsuleFrame) {
+  std::string input = absl::HexStringToBytes(
+      "80ffcab5"    // type (CAPSULE)
+      "05"          // length
+      "03"          // capsule type (REGISTER_DATAGRAM_NO_CONTEXT)
+      "01020304");  // context extensions
+
+  CapsuleFrame frame(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT);
+  std::array<char, 4> context_extensions = {1, 2, 3, 4};
+  frame.register_datagram_no_context_capsule.context_extensions =
+      absl::string_view(context_extensions.data(), context_extensions.size());
+  EXPECT_CALL(visitor_, OnCapsuleFrame(frame));
+  EXPECT_EQ(ProcessInput(input), input.size());
+}
+
 TEST_F(HttpDecoderTest, WebTransportStreamDisabled) {
   InSequence s;
 
diff --git a/quic/core/http/http_encoder.cc b/quic/core/http/http_encoder.cc
index 25f6200..d9d161b 100644
--- a/quic/core/http/http_encoder.cc
+++ b/quic/core/http/http_encoder.cc
@@ -276,4 +276,143 @@
   return 0;
 }
 
+// static
+QuicByteCount HttpEncoder::SerializeCapsuleFrame(
+    const CapsuleFrame& capsule_frame, std::unique_ptr<char[]>* output) {
+  QuicByteCount capsule_type_length = QuicDataWriter::GetVarInt62Len(
+      static_cast<uint64_t>(capsule_frame.capsule_type));
+  QuicByteCount capsule_data_length;
+  switch (capsule_frame.capsule_type) {
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      capsule_data_length =
+          QuicDataWriter::GetVarInt62Len(
+              capsule_frame.register_datagram_context_capsule.context_id) +
+          capsule_frame.register_datagram_context_capsule.context_extensions
+              .length();
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      capsule_data_length =
+          QuicDataWriter::GetVarInt62Len(
+              capsule_frame.close_datagram_context_capsule.context_id) +
+          capsule_frame.close_datagram_context_capsule.context_extensions
+              .length();
+      break;
+    case CapsuleType::DATAGRAM:
+      capsule_data_length =
+          capsule_frame.datagram_capsule.http_datagram_payload.length();
+      if (capsule_frame.datagram_capsule.context_id.has_value()) {
+        capsule_data_length += QuicDataWriter::GetVarInt62Len(
+            capsule_frame.datagram_capsule.context_id.value());
+      }
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      capsule_data_length = capsule_frame.register_datagram_no_context_capsule
+                                .context_extensions.length();
+      break;
+    default:
+      capsule_data_length = capsule_frame.unknown_capsule_data.length();
+      break;
+  }
+  QuicByteCount frame_length_field_value =
+      capsule_type_length + capsule_data_length;
+  QuicByteCount total_frame_length =
+      QuicDataWriter::GetVarInt62Len(
+          static_cast<uint64_t>(HttpFrameType::CAPSULE)) +
+      QuicDataWriter::GetVarInt62Len(frame_length_field_value) +
+      capsule_type_length + capsule_data_length;
+  *output = std::make_unique<char[]>(total_frame_length);
+  QuicDataWriter writer(total_frame_length, output->get());
+  if (!writer.WriteVarInt62(static_cast<uint64_t>(HttpFrameType::CAPSULE))) {
+    QUIC_BUG(capsule frame type write fail)
+        << "Failed to write CAPSULE frame type";
+    return 0;
+  }
+  if (!writer.WriteVarInt62(frame_length_field_value)) {
+    QUIC_BUG(capsule frame length write fail)
+        << "Failed to write CAPSULE frame length";
+    return 0;
+  }
+  if (!writer.WriteVarInt62(
+          static_cast<uint64_t>(capsule_frame.capsule_type))) {
+    QUIC_BUG(capsule type write fail) << "Failed to write CAPSULE type";
+    return 0;
+  }
+  switch (capsule_frame.capsule_type) {
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      if (!writer.WriteVarInt62(
+              capsule_frame.register_datagram_context_capsule.context_id)) {
+        QUIC_BUG(register context capsule context ID write fail)
+            << "Failed to write REGISTER_DATAGRAM_CONTEXT CAPSULE context ID";
+        return 0;
+      }
+      if (!writer.WriteBytes(capsule_frame.register_datagram_context_capsule
+                                 .context_extensions.data(),
+                             capsule_frame.register_datagram_context_capsule
+                                 .context_extensions.length())) {
+        QUIC_BUG(register context capsule extensions write fail)
+            << "Failed to write REGISTER_DATAGRAM_CONTEXT CAPSULE extensions";
+        return 0;
+      }
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      if (!writer.WriteVarInt62(
+              capsule_frame.close_datagram_context_capsule.context_id)) {
+        QUIC_BUG(close context capsule context ID write fail)
+            << "Failed to write CLOSE_DATAGRAM_CONTEXT CAPSULE context ID";
+        return 0;
+      }
+      if (!writer.WriteBytes(capsule_frame.close_datagram_context_capsule
+                                 .context_extensions.data(),
+                             capsule_frame.close_datagram_context_capsule
+                                 .context_extensions.length())) {
+        QUIC_BUG(close context capsule extensions write fail)
+            << "Failed to write CLOSE_DATAGRAM_CONTEXT CAPSULE extensions";
+        return 0;
+      }
+      break;
+    case CapsuleType::DATAGRAM:
+      if (capsule_frame.datagram_capsule.context_id.has_value()) {
+        if (!writer.WriteVarInt62(
+                capsule_frame.datagram_capsule.context_id.value())) {
+          QUIC_BUG(datagram capsule context ID write fail)
+              << "Failed to write DATAGRAM CAPSULE context ID";
+          return 0;
+        }
+      }
+      if (!writer.WriteBytes(
+              capsule_frame.datagram_capsule.http_datagram_payload.data(),
+              capsule_frame.datagram_capsule.http_datagram_payload.length())) {
+        QUIC_BUG(datagram capsule payload write fail)
+            << "Failed to write DATAGRAM CAPSULE payload";
+        return 0;
+      }
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      if (!writer.WriteBytes(capsule_frame.register_datagram_no_context_capsule
+                                 .context_extensions.data(),
+                             capsule_frame.register_datagram_no_context_capsule
+                                 .context_extensions.length())) {
+        QUIC_BUG(register no context capsule extensions write fail)
+            << "Failed to write REGISTER_DATAGRAM_NO_CONTEXT CAPSULE "
+               "extensions";
+        return 0;
+      }
+      break;
+    default:
+      if (!writer.WriteBytes(capsule_frame.unknown_capsule_data.data(),
+                             capsule_frame.unknown_capsule_data.length())) {
+        QUIC_BUG(capsule data write fail) << "Failed to write CAPSULE data";
+        return 0;
+      }
+      break;
+  }
+  if (writer.remaining() != 0) {
+    QUIC_BUG(capsule write length mismatch)
+        << "CAPSULE serialization wrote " << writer.length() << " instead of "
+        << writer.capacity();
+    return 0;
+  }
+  return total_frame_length;
+}
+
 }  // namespace quic
diff --git a/quic/core/http/http_encoder.h b/quic/core/http/http_encoder.h
index 585e739..6c8ab49 100644
--- a/quic/core/http/http_encoder.h
+++ b/quic/core/http/http_encoder.h
@@ -66,6 +66,10 @@
   static QuicByteCount SerializeWebTransportStreamFrameHeader(
       WebTransportSessionId session_id,
       std::unique_ptr<char[]>* output);
+
+  // Serializes a CAPSULE frame as specified in draft-ietf-masque-h3-datagram.
+  static QuicByteCount SerializeCapsuleFrame(const CapsuleFrame& capsule_frame,
+                                             std::unique_ptr<char[]>* output);
 };
 
 }  // namespace quic
diff --git a/quic/core/http/http_encoder_test.cc b/quic/core/http/http_encoder_test.cc
index c2b0f36..144c414 100644
--- a/quic/core/http/http_encoder_test.cc
+++ b/quic/core/http/http_encoder_test.cc
@@ -5,6 +5,7 @@
 #include "quic/core/http/http_encoder.h"
 
 #include "absl/base/macros.h"
+#include "quic/core/http/http_frames.h"
 #include "quic/core/quic_simple_buffer_allocator.h"
 #include "quic/platform/api/quic_flags.h"
 #include "quic/platform/api/quic_test.h"
@@ -138,5 +139,62 @@
       "WEBTRANSPORT_STREAM", buffer.get(), length, output, sizeof(output));
 }
 
+TEST(HttpEncoderTest, SerializeRegisterDatagramContextCapsule) {
+  CapsuleFrame capsule_frame(CapsuleType::REGISTER_DATAGRAM_CONTEXT);
+  capsule_frame.register_datagram_context_capsule.context_id = 4;
+  uint8_t output[] = {0x80, 0xff, 0xca, 0xb5,  // type (CAPSULE)
+                      0x02,                    // frame length.
+                      0x00,   // capsule type (REGISTER_DATAGRAM_CONTEXT).
+                      0x04};  // context ID.
+  std::unique_ptr<char[]> buffer;
+  uint64_t length = HttpEncoder::SerializeCapsuleFrame(capsule_frame, &buffer);
+  quiche::test::CompareCharArraysWithHexError(
+      "REGISTER_DATAGRAM_CONTEXT", buffer.get(), length,
+      reinterpret_cast<char*>(output), sizeof(output));
+}
+
+TEST(HttpEncoderTest, SerializeCloseDatagramContextCapsule) {
+  CapsuleFrame capsule_frame(CapsuleType::CLOSE_DATAGRAM_CONTEXT);
+  capsule_frame.close_datagram_context_capsule.context_id = 4;
+  uint8_t output[] = {0x80, 0xff, 0xca, 0xb5,  // type (CAPSULE)
+                      0x02,                    // frame length.
+                      0x01,   // capsule type (CLOSE_DATAGRAM_CONTEXT).
+                      0x04};  // context ID.
+  std::unique_ptr<char[]> buffer;
+  uint64_t length = HttpEncoder::SerializeCapsuleFrame(capsule_frame, &buffer);
+  quiche::test::CompareCharArraysWithHexError(
+      "CLOSE_DATAGRAM_CONTEXT", buffer.get(), length,
+      reinterpret_cast<char*>(output), sizeof(output));
+}
+
+TEST(HttpEncoderTest, SerializeDatagramCapsule) {
+  uint8_t http_datagram_payload[5] = {0x21, 0x22, 0x23, 0x24, 0x25};
+  CapsuleFrame capsule_frame(CapsuleType::DATAGRAM);
+  capsule_frame.datagram_capsule.http_datagram_payload =
+      absl::string_view(reinterpret_cast<char*>(http_datagram_payload),
+                        sizeof(http_datagram_payload));
+  uint8_t output[] = {0x80, 0xff, 0xca, 0xb5,  // type (CAPSULE)
+                      0x06,                    // frame length.
+                      0x02,                    // capsule type (DATAGRAM).
+                      0x21, 0x22, 0x23, 0x24, 0x25};  // payload.
+  std::unique_ptr<char[]> buffer;
+  uint64_t length = HttpEncoder::SerializeCapsuleFrame(capsule_frame, &buffer);
+  quiche::test::CompareCharArraysWithHexError("DATAGRAM", buffer.get(), length,
+                                              reinterpret_cast<char*>(output),
+                                              sizeof(output));
+}
+
+TEST(HttpEncoderTest, SerializeRegisterDatagramNoContextCapsule) {
+  CapsuleFrame capsule_frame(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT);
+  uint8_t output[] = {0x80, 0xff, 0xca, 0xb5,  // type (CAPSULE)
+                      0x01,                    // frame length.
+                      0x03};  // capsule type (REGISTER_DATAGRAM_NO_CONTEXT).
+  std::unique_ptr<char[]> buffer;
+  uint64_t length = HttpEncoder::SerializeCapsuleFrame(capsule_frame, &buffer);
+  quiche::test::CompareCharArraysWithHexError(
+      "REGISTER_DATAGRAM_NO_CONTEXT", buffer.get(), length,
+      reinterpret_cast<char*>(output), sizeof(output));
+}
+
 }  // namespace test
 }  // namespace quic
diff --git a/quic/core/http/http_frames.h b/quic/core/http/http_frames.h
index 6a00ad7..d678062 100644
--- a/quic/core/http/http_frames.h
+++ b/quic/core/http/http_frames.h
@@ -7,6 +7,7 @@
 
 #include <algorithm>
 #include <cstdint>
+#include <limits>
 #include <map>
 #include <ostream>
 #include <sstream>
@@ -14,6 +15,7 @@
 #include "absl/container/flat_hash_map.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
 #include "quic/core/http/http_constants.h"
 #include "quic/core/quic_types.h"
 #include "spdy/core/spdy_protocol.h"
@@ -37,6 +39,8 @@
   PRIORITY_UPDATE_REQUEST_STREAM = 0xF0700,
   // https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-00.html
   WEBTRANSPORT_STREAM = 0x41,
+  // https://datatracker.ietf.org/doc/html/draft-ietf-masque-h3-datagram-03
+  CAPSULE = 0xffcab5,
 };
 
 // 7.2.1.  DATA
@@ -182,6 +186,179 @@
   }
 };
 
+enum class CapsuleType : uint64_t {
+  // Casing in this enum matches the IETF specification.
+  REGISTER_DATAGRAM_CONTEXT = 0x00,
+  CLOSE_DATAGRAM_CONTEXT = 0x01,
+  DATAGRAM = 0x02,
+  REGISTER_DATAGRAM_NO_CONTEXT = 0x03,
+};
+
+inline std::string CapsuleTypeToString(CapsuleType capsule_type) {
+  switch (capsule_type) {
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      return "REGISTER_DATAGRAM_CONTEXT";
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      return "CLOSE_DATAGRAM_CONTEXT";
+    case CapsuleType::DATAGRAM:
+      return "DATAGRAM";
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      return "REGISTER_DATAGRAM_NO_CONTEXT";
+  }
+  return absl::StrCat("Unknown(", static_cast<uint64_t>(capsule_type), ")");
+}
+
+inline std::ostream& operator<<(std::ostream& os,
+                                const CapsuleType& capsule_type) {
+  os << CapsuleTypeToString(capsule_type);
+  return os;
+}
+
+// CAPSULE HTTP frame from draft-ietf-masque-h3-datagram.
+struct QUIC_EXPORT_PRIVATE CapsuleFrame {
+  CapsuleType capsule_type;
+  union {
+    struct {
+      QuicDatagramContextId context_id;
+      absl::string_view context_extensions;
+    } register_datagram_context_capsule;
+    struct {
+      QuicDatagramContextId context_id;
+      absl::string_view context_extensions;
+    } close_datagram_context_capsule;
+    struct {
+      absl::optional<QuicDatagramContextId> context_id;
+      absl::string_view http_datagram_payload;
+    } datagram_capsule;
+    struct {
+      absl::string_view context_extensions;
+    } register_datagram_no_context_capsule;
+    absl::string_view unknown_capsule_data;
+  };
+
+  explicit CapsuleFrame(CapsuleType capsule_type) : capsule_type(capsule_type) {
+    switch (capsule_type) {
+      case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+        register_datagram_context_capsule.context_id = 0;
+        register_datagram_context_capsule.context_extensions =
+            absl::string_view();
+        break;
+      case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+        close_datagram_context_capsule.context_id = 0;
+        close_datagram_context_capsule.context_extensions = absl::string_view();
+        break;
+      case CapsuleType::DATAGRAM:
+        datagram_capsule.context_id = absl::nullopt;
+        datagram_capsule.http_datagram_payload = absl::string_view();
+        break;
+      case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+        register_datagram_no_context_capsule.context_extensions =
+            absl::string_view();
+        break;
+      default:
+        unknown_capsule_data = absl::string_view();
+        break;
+    }
+  }
+
+  CapsuleFrame()
+      : CapsuleFrame(
+            static_cast<CapsuleType>(std::numeric_limits<uint64_t>::max())) {}
+
+  CapsuleFrame& operator=(const CapsuleFrame& other) {
+    capsule_type = other.capsule_type;
+    switch (capsule_type) {
+      case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+        register_datagram_context_capsule.context_id =
+            other.register_datagram_context_capsule.context_id;
+        register_datagram_context_capsule.context_extensions =
+            other.register_datagram_context_capsule.context_extensions;
+        break;
+      case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+        close_datagram_context_capsule.context_id =
+            other.close_datagram_context_capsule.context_id;
+        close_datagram_context_capsule.context_extensions =
+            other.close_datagram_context_capsule.context_extensions;
+        break;
+      case CapsuleType::DATAGRAM:
+        datagram_capsule.context_id = other.datagram_capsule.context_id;
+        datagram_capsule.http_datagram_payload =
+            other.datagram_capsule.http_datagram_payload;
+        break;
+      case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+        register_datagram_no_context_capsule.context_extensions =
+            other.register_datagram_no_context_capsule.context_extensions;
+        break;
+      default:
+        unknown_capsule_data = other.unknown_capsule_data;
+        break;
+    }
+    return *this;
+  }
+
+  CapsuleFrame(const CapsuleFrame& other) : CapsuleFrame(other.capsule_type) {
+    *this = other;
+  }
+
+  bool operator==(const CapsuleFrame& other) const {
+    if (capsule_type != other.capsule_type) {
+      return false;
+    }
+    switch (capsule_type) {
+      case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+        return register_datagram_context_capsule.context_id ==
+                   other.register_datagram_context_capsule.context_id &&
+               register_datagram_context_capsule.context_extensions ==
+                   other.register_datagram_context_capsule.context_extensions;
+      case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+        return close_datagram_context_capsule.context_id ==
+                   other.close_datagram_context_capsule.context_id &&
+               close_datagram_context_capsule.context_extensions ==
+                   other.close_datagram_context_capsule.context_extensions;
+      case CapsuleType::DATAGRAM:
+        return datagram_capsule.context_id ==
+                   other.datagram_capsule.context_id &&
+               datagram_capsule.http_datagram_payload ==
+                   other.datagram_capsule.http_datagram_payload;
+      case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+        return register_datagram_no_context_capsule.context_extensions ==
+               other.register_datagram_no_context_capsule.context_extensions;
+      default:
+        return unknown_capsule_data == other.unknown_capsule_data;
+    }
+  }
+
+  std::string ToString() const {
+    std::string rv = CapsuleTypeToString(capsule_type);
+    switch (capsule_type) {
+      case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+        absl::StrAppend(&rv, "(", register_datagram_context_capsule.context_id,
+                        ")");
+        break;
+      case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+        absl::StrAppend(&rv, "(", close_datagram_context_capsule.context_id,
+                        ")");
+        break;
+      case CapsuleType::DATAGRAM:
+        if (datagram_capsule.context_id.has_value()) {
+          absl::StrAppend(&rv, "(", datagram_capsule.context_id.value(), ")");
+        }
+        break;
+      case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+        break;
+      default:
+        break;
+    }
+    return rv;
+  }
+
+  friend QUIC_EXPORT_PRIVATE std::ostream& operator<<(
+      std::ostream& os, const CapsuleFrame& frame) {
+    os << frame.ToString();
+    return os;
+  }
+};
+
 }  // namespace quic
 
 #endif  // QUICHE_QUIC_CORE_HTTP_HTTP_FRAMES_H_
diff --git a/quic/core/http/quic_receive_control_stream.cc b/quic/core/http/quic_receive_control_stream.cc
index 9c164d0..b36bc7f 100644
--- a/quic/core/http/quic_receive_control_stream.cc
+++ b/quic/core/http/quic_receive_control_stream.cc
@@ -200,6 +200,10 @@
   return true;
 }
 
+bool QuicReceiveControlStream::OnCapsuleFrame(const CapsuleFrame& /*frame*/) {
+  return ValidateFrameType(HttpFrameType::CAPSULE);
+}
+
 void QuicReceiveControlStream::OnWebTransportStreamFrameType(
     QuicByteCount /*header_length*/,
     WebTransportSessionId /*session_id*/) {
@@ -237,7 +241,8 @@
       (spdy_session()->perspective() == Perspective::IS_CLIENT &&
        frame_type == HttpFrameType::MAX_PUSH_ID) ||
       (spdy_session()->perspective() == Perspective::IS_SERVER &&
-       frame_type == HttpFrameType::ACCEPT_CH)) {
+       frame_type == HttpFrameType::ACCEPT_CH) ||
+      frame_type == HttpFrameType::CAPSULE) {
     stream_delegate()->OnStreamError(
         QUIC_HTTP_FRAME_UNEXPECTED_ON_CONTROL_STREAM,
         absl::StrCat("Invalid frame type ", static_cast<int>(frame_type),
diff --git a/quic/core/http/quic_receive_control_stream.h b/quic/core/http/quic_receive_control_stream.h
index 3c61d3c..3390df0 100644
--- a/quic/core/http/quic_receive_control_stream.h
+++ b/quic/core/http/quic_receive_control_stream.h
@@ -51,6 +51,7 @@
   bool OnPriorityUpdateFrame(const PriorityUpdateFrame& frame) override;
   bool OnAcceptChFrameStart(QuicByteCount header_length) override;
   bool OnAcceptChFrame(const AcceptChFrame& frame) override;
+  bool OnCapsuleFrame(const CapsuleFrame& frame) override;
   void OnWebTransportStreamFrameType(QuicByteCount header_length,
                                      WebTransportSessionId session_id) override;
   bool OnUnknownFrameStart(uint64_t frame_type,
diff --git a/quic/core/http/quic_send_control_stream_test.cc b/quic/core/http/quic_send_control_stream_test.cc
index 3066cb3..7823b0f 100644
--- a/quic/core/http/quic_send_control_stream_test.cc
+++ b/quic/core/http/quic_send_control_stream_test.cc
@@ -135,22 +135,24 @@
       "61");  //  payload "a"
   if (QuicSpdySessionPeer::ShouldNegotiateHttp3Datagram(&session_)) {
     expected_write_data = absl::HexStringToBytes(
-        "00"    // stream type: control stream
-        "04"    // frame type: SETTINGS frame
-        "0e"    // frame length
-        "01"    // SETTINGS_QPACK_MAX_TABLE_CAPACITY
-        "40ff"  // 255
-        "06"    // SETTINGS_MAX_HEADER_LIST_SIZE
-        "4400"  // 1024
-        "07"    // SETTINGS_QPACK_BLOCKED_STREAMS
-        "10"    // 16
-        "4040"  // 0x40 as the reserved settings id
-        "14"    // 20
-        "4276"  // SETTINGS_H3_DATAGRAM
-        "01"    // 1
-        "4040"  // 0x40 as the reserved frame type
-        "01"    // 1 byte frame length
-        "61");  //  payload "a"
+        "00"         // stream type: control stream
+        "04"         // frame type: SETTINGS frame
+        "0e"         // frame length
+        "01"         // SETTINGS_QPACK_MAX_TABLE_CAPACITY
+        "40ff"       // 255
+        "06"         // SETTINGS_MAX_HEADER_LIST_SIZE
+        "4400"       // 1024
+        "07"         // SETTINGS_QPACK_BLOCKED_STREAMS
+        "10"         // 16
+        "4040"       // 0x40 as the reserved settings id
+        "14"         // 20
+        "4276"       // SETTINGS_H3_DATAGRAM_DRAFT00
+        "01"         // 1
+        "800ffd276"  // SETTINGS_H3_DATAGRAM_DRAFT03
+        "01"         // 1
+        "4040"       // 0x40 as the reserved frame type
+        "01"         // 1 byte frame length
+        "61");       //  payload "a"
   }
 
   auto buffer = std::make_unique<char[]>(expected_write_data.size());
diff --git a/quic/core/http/quic_spdy_session.cc b/quic/core/http/quic_spdy_session.cc
index dc773ae..f01da54 100644
--- a/quic/core/http/quic_spdy_session.cc
+++ b/quic/core/http/quic_spdy_session.cc
@@ -135,6 +135,10 @@
     session_->OnAcceptChFrameReceivedViaAlps(frame);
     return true;
   }
+  bool OnCapsuleFrame(const CapsuleFrame& /*frame*/) override {
+    error_detail_ = "CAPSULE frame forbidden";
+    return false;
+  }
   void OnWebTransportStreamFrameType(
       QuicByteCount /*header_length*/,
       WebTransportSessionId /*session_id*/) override {
@@ -521,7 +525,8 @@
   settings_.values[SETTINGS_MAX_FIELD_SECTION_SIZE] =
       max_inbound_header_list_size_;
   if (ShouldNegotiateHttp3Datagram() && version().UsesHttp3()) {
-    settings_.values[SETTINGS_H3_DATAGRAM] = 1;
+    settings_.values[SETTINGS_H3_DATAGRAM_DRAFT00] = 1;
+    settings_.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1;
   }
   if (WillNegotiateWebTransport()) {
     settings_.values[SETTINGS_WEBTRANS_DRAFT00] = 1;
@@ -1126,24 +1131,59 @@
             absl::StrCat("received HTTP/2 specific setting in HTTP/3 session: ",
                          id));
         return false;
-      case SETTINGS_H3_DATAGRAM: {
+      case SETTINGS_H3_DATAGRAM_DRAFT00: {
         if (!ShouldNegotiateHttp3Datagram()) {
           break;
         }
-        QUIC_DVLOG(1) << ENDPOINT << "SETTINGS_H3_DATAGRAM received with value "
+        QUIC_DVLOG(1) << ENDPOINT
+                      << "SETTINGS_H3_DATAGRAM_DRAFT00 received with value "
                       << value;
         if (!version().UsesHttp3()) {
           break;
         }
         if (value != 0 && value != 1) {
           std::string error_details = absl::StrCat(
-              "received SETTINGS_H3_DATAGRAM with invalid value ", value);
-          QUIC_PEER_BUG(quic_peer_bug_10360_7) << ENDPOINT << error_details;
+              "received SETTINGS_H3_DATAGRAM_DRAFT00 with invalid value ",
+              value);
+          QUIC_PEER_BUG(bad SETTINGS_H3_DATAGRAM_DRAFT00)
+              << ENDPOINT << error_details;
           CloseConnectionWithDetails(QUIC_HTTP_RECEIVE_SPDY_SETTING,
                                      error_details);
           return false;
         }
-        h3_datagram_supported_ = !!value;
+        if (value && http_datagram_support_ != HttpDatagramSupport::kDraft03) {
+          // If both draft-00 and draft-03 are supported, use draft-03.
+          http_datagram_support_ = HttpDatagramSupport::kDraft00;
+#if 0
+          // DO_NOT_SUBMIT hack around Ericsson bug (they're sending 00 instead of 03):
+          http_datagram_support_ = HttpDatagramSupport::kDraft03;
+#endif  // 0
+        }
+        break;
+      }
+      case SETTINGS_H3_DATAGRAM_DRAFT03: {
+        if (!ShouldNegotiateHttp3Datagram()) {
+          break;
+        }
+        QUIC_DVLOG(1) << ENDPOINT
+                      << "SETTINGS_H3_DATAGRAM_DRAFT03 received with value "
+                      << value;
+        if (!version().UsesHttp3()) {
+          break;
+        }
+        if (value != 0 && value != 1) {
+          std::string error_details = absl::StrCat(
+              "received SETTINGS_H3_DATAGRAM_DRAFT03 with invalid value ",
+              value);
+          QUIC_PEER_BUG(bad SETTINGS_H3_DATAGRAM_DRAFT03)
+              << ENDPOINT << error_details;
+          CloseConnectionWithDetails(QUIC_HTTP_RECEIVE_SPDY_SETTING,
+                                     error_details);
+          return false;
+        }
+        if (value) {
+          http_datagram_support_ = HttpDatagramSupport::kDraft03;
+        }
         break;
       }
       case SETTINGS_WEBTRANS_DRAFT00:
@@ -1616,15 +1656,24 @@
     QuicDatagramStreamId stream_id,
     absl::optional<QuicDatagramContextId> context_id,
     absl::string_view payload) {
+  if (!SupportsH3Datagram()) {
+    QUIC_BUG(send http datagram too early)
+        << "Refusing to send HTTP Datagram before SETTINGS received";
+    return MESSAGE_STATUS_INTERNAL_ERROR;
+  }
+  uint64_t stream_id_to_write = stream_id;
+  if (http_datagram_support_ != HttpDatagramSupport::kDraft00) {
+    stream_id_to_write /= kHttpDatagramStreamIdDivisor;
+  }
   size_t slice_length =
-      QuicDataWriter::GetVarInt62Len(stream_id) + payload.length();
+      QuicDataWriter::GetVarInt62Len(stream_id_to_write) + payload.length();
   if (context_id.has_value()) {
     slice_length += QuicDataWriter::GetVarInt62Len(context_id.value());
   }
   QuicBuffer buffer(connection()->helper()->GetStreamSendBufferAllocator(),
                     slice_length);
   QuicDataWriter writer(slice_length, buffer.data());
-  if (!writer.WriteVarInt62(stream_id)) {
+  if (!writer.WriteVarInt62(stream_id_to_write)) {
     QUIC_BUG(h3 datagram stream ID write fail)
         << "Failed to write HTTP/3 datagram stream ID";
     return MESSAGE_STATUS_INTERNAL_ERROR;
@@ -1665,8 +1714,8 @@
 
 void QuicSpdySession::OnMessageReceived(absl::string_view message) {
   QuicSession::OnMessageReceived(message);
-  if (!h3_datagram_supported_) {
-    QUIC_DLOG(ERROR) << "Ignoring unexpected received HTTP/3 datagram";
+  if (!SupportsH3Datagram()) {
+    QUIC_DLOG(INFO) << "Ignoring unexpected received HTTP/3 datagram";
     return;
   }
   QuicDataReader reader(message);
@@ -1675,7 +1724,11 @@
     QUIC_DLOG(ERROR) << "Failed to parse stream ID in received HTTP/3 datagram";
     return;
   }
-  if (perspective() == Perspective::IS_SERVER) {
+  if (http_datagram_support_ != HttpDatagramSupport::kDraft00) {
+    stream_id64 *= kHttpDatagramStreamIdDivisor;
+  }
+  if (perspective() == Perspective::IS_SERVER &&
+      http_datagram_support_ == HttpDatagramSupport::kDraft00) {
     auto it = h3_datagram_flow_id_to_stream_id_map_.find(stream_id64);
     if (it == h3_datagram_flow_id_to_stream_id_map_.end()) {
       QUIC_DLOG(INFO) << "Received unknown HTTP/3 datagram flow ID "
@@ -1705,10 +1758,14 @@
 }
 
 bool QuicSpdySession::SupportsWebTransport() {
-  return WillNegotiateWebTransport() && h3_datagram_supported_ &&
+  return WillNegotiateWebTransport() && SupportsH3Datagram() &&
          peer_supports_webtransport_;
 }
 
+bool QuicSpdySession::SupportsH3Datagram() const {
+  return http_datagram_support_ != HttpDatagramSupport::kNone;
+}
+
 WebTransportHttp3* QuicSpdySession::GetWebTransportSession(
     WebTransportSessionId id) {
   if (!SupportsWebTransport()) {
@@ -1840,6 +1897,25 @@
   return false;
 }
 
+std::string HttpDatagramSupportToString(
+    HttpDatagramSupport http_datagram_support) {
+  switch (http_datagram_support) {
+    case HttpDatagramSupport::kNone:
+      return "None";
+    case HttpDatagramSupport::kDraft00:
+      return "Draft00";
+    case HttpDatagramSupport::kDraft03:
+      return "Draft03";
+  }
+  return absl::StrCat("Unknown(", static_cast<int>(http_datagram_support), ")");
+}
+
+std::ostream& operator<<(std::ostream& os,
+                         const HttpDatagramSupport& http_datagram_support) {
+  os << HttpDatagramSupportToString(http_datagram_support);
+  return os;
+}
+
 #undef ENDPOINT  // undef for jumbo builds
 
 }  // namespace quic
diff --git a/quic/core/http/quic_spdy_session.h b/quic/core/http/quic_spdy_session.h
index 8085dd8..c471903 100644
--- a/quic/core/http/quic_spdy_session.h
+++ b/quic/core/http/quic_spdy_session.h
@@ -6,6 +6,7 @@
 #define QUICHE_QUIC_CORE_HTTP_QUIC_SPDY_SESSION_H_
 
 #include <cstddef>
+#include <cstdint>
 #include <list>
 #include <memory>
 #include <string>
@@ -119,6 +120,19 @@
   virtual void OnSettingsFrameResumed(const SettingsFrame& /*frame*/) {}
 };
 
+// Whether draft-ietf-masque-h3-datagram is supported on this session and if so
+// which draft is currently in use.
+enum class HttpDatagramSupport : uint8_t {
+  kNone = 0,  // HTTP Datagrams are not supported for this session.
+  kDraft00 = 1,
+  kDraft03 = 2,
+};
+
+QUIC_EXPORT_PRIVATE std::string HttpDatagramSupportToString(
+    HttpDatagramSupport http_datagram_support);
+QUIC_EXPORT_PRIVATE std::ostream& operator<<(
+    std::ostream& os, const HttpDatagramSupport& http_datagram_support);
+
 // A QUIC session for HTTP.
 class QUIC_EXPORT_PRIVATE QuicSpdySession
     : public QuicSession,
@@ -374,9 +388,11 @@
   // extension.
   virtual void OnAcceptChFrameReceivedViaAlps(const AcceptChFrame& /*frame*/);
 
-  // Whether HTTP/3 datagrams are supported on this session, based on received
-  // SETTINGS.
-  bool h3_datagram_supported() const { return h3_datagram_supported_; }
+  // Whether HTTP datagrams are supported on this session and which draft is in
+  // use, based on received SETTINGS.
+  HttpDatagramSupport http_datagram_support() const {
+    return http_datagram_support_;
+  }
 
   // This must not be used except by QuicSpdyStream::SendHttp3Datagram.
   MessageStatus SendHttp3Datagram(
@@ -400,7 +416,7 @@
   bool SupportsWebTransport();
 
   // Indicates whether both the peer and us support HTTP/3 Datagrams.
-  bool SupportsH3Datagram() { return h3_datagram_supported_; }
+  bool SupportsH3Datagram() const;
 
   // Indicates whether the HTTP/3 session will indicate WebTransport support to
   // the peer.
@@ -654,8 +670,9 @@
   // frame has been sent yet.
   absl::optional<uint64_t> last_sent_http3_goaway_id_;
 
-  // Whether both this endpoint and our peer support HTTP/3 datagrams.
-  bool h3_datagram_supported_ = false;
+  // Whether both this endpoint and our peer support HTTP datagrams and which
+  // draft is in use for this session.
+  HttpDatagramSupport http_datagram_support_ = HttpDatagramSupport::kNone;
 
   // Whether the peer has indicated WebTransport support.
   bool peer_supports_webtransport_ = false;
diff --git a/quic/core/http/quic_spdy_session_test.cc b/quic/core/http/quic_spdy_session_test.cc
index c42b8ff..371a8c3 100644
--- a/quic/core/http/quic_spdy_session_test.cc
+++ b/quic/core/http/quic_spdy_session_test.cc
@@ -549,7 +549,7 @@
 
   void ReceiveWebTransportSettings() {
     SettingsFrame settings;
-    settings.values[SETTINGS_H3_DATAGRAM] = 1;
+    settings.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1;
     settings.values[SETTINGS_WEBTRANS_DRAFT00] = 1;
     std::string data =
         std::string(1, kControlStream) + EncodeSettings(settings);
@@ -573,8 +573,14 @@
     headers.OnHeaderBlockStart();
     headers.OnHeader(":method", "CONNECT");
     headers.OnHeader(":protocol", "webtransport");
-    headers.OnHeader("datagram-flow-id", absl::StrCat(session_id));
+    if (session_.http_datagram_support() == HttpDatagramSupport::kDraft00) {
+      headers.OnHeader("datagram-flow-id", absl::StrCat(session_id));
+    }
     stream->OnStreamHeaderList(/*fin=*/true, 0, headers);
+    if (session_.http_datagram_support() == HttpDatagramSupport::kDraft03) {
+      stream->OnCapsuleFrame(
+          CapsuleFrame(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT));
+    }
     WebTransportHttp3* web_transport =
         session_.GetWebTransportSession(session_id);
     ASSERT_TRUE(web_transport != nullptr);
@@ -3404,16 +3410,17 @@
   EXPECT_EQ("multiple SETTINGS frames", error.value());
 }
 
-TEST_P(QuicSpdySessionTestClient, H3DatagramSetting) {
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingDraft00) {
   if (!version().UsesHttp3()) {
     return;
   }
   session_.set_should_negotiate_h3_datagram(true);
   // HTTP/3 datagrams aren't supported before SETTINGS are received.
-  EXPECT_FALSE(session_.h3_datagram_supported());
+  EXPECT_FALSE(session_.SupportsH3Datagram());
+  EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kNone);
   // Receive SETTINGS.
   SettingsFrame settings;
-  settings.values[SETTINGS_H3_DATAGRAM] = 1;
+  settings.values[SETTINGS_H3_DATAGRAM_DRAFT00] = 1;
   std::string data = std::string(1, kControlStream) + EncodeSettings(settings);
   QuicStreamId stream_id =
       GetNthServerInitiatedUnidirectionalStreamId(transport_version(), 3);
@@ -3424,7 +3431,59 @@
   EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(settings));
   session_.OnStreamFrame(frame);
   // HTTP/3 datagrams are now supported.
-  EXPECT_TRUE(session_.h3_datagram_supported());
+  EXPECT_TRUE(session_.SupportsH3Datagram());
+  EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kDraft00);
+}
+
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingDraft03) {
+  if (!version().UsesHttp3()) {
+    return;
+  }
+  session_.set_should_negotiate_h3_datagram(true);
+  // HTTP/3 datagrams aren't supported before SETTINGS are received.
+  EXPECT_FALSE(session_.SupportsH3Datagram());
+  EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kNone);
+  // Receive SETTINGS.
+  SettingsFrame settings;
+  settings.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1;
+  std::string data = std::string(1, kControlStream) + EncodeSettings(settings);
+  QuicStreamId stream_id =
+      GetNthServerInitiatedUnidirectionalStreamId(transport_version(), 3);
+  QuicStreamFrame frame(stream_id, /*fin=*/false, /*offset=*/0, data);
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session_.set_debug_visitor(&debug_visitor);
+  EXPECT_CALL(debug_visitor, OnPeerControlStreamCreated(stream_id));
+  EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(settings));
+  session_.OnStreamFrame(frame);
+  // HTTP/3 datagrams are now supported.
+  EXPECT_TRUE(session_.SupportsH3Datagram());
+  EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kDraft03);
+}
+
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingDraft00And03) {
+  if (!version().UsesHttp3()) {
+    return;
+  }
+  session_.set_should_negotiate_h3_datagram(true);
+  // HTTP/3 datagrams aren't supported before SETTINGS are received.
+  EXPECT_FALSE(session_.SupportsH3Datagram());
+  EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kNone);
+  // Receive SETTINGS.
+  SettingsFrame settings;
+  settings.values[SETTINGS_H3_DATAGRAM_DRAFT00] = 1;
+  settings.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1;
+  std::string data = std::string(1, kControlStream) + EncodeSettings(settings);
+  QuicStreamId stream_id =
+      GetNthServerInitiatedUnidirectionalStreamId(transport_version(), 3);
+  QuicStreamFrame frame(stream_id, /*fin=*/false, /*offset=*/0, data);
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session_.set_debug_visitor(&debug_visitor);
+  EXPECT_CALL(debug_visitor, OnPeerControlStreamCreated(stream_id));
+  EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(settings));
+  session_.OnStreamFrame(frame);
+  // HTTP/3 datagrams are now supported.
+  EXPECT_TRUE(session_.SupportsH3Datagram());
+  EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kDraft03);
 }
 
 TEST_P(QuicSpdySessionTestClient, WebTransportSetting) {
@@ -3444,7 +3503,7 @@
   CompleteHandshake();
 
   SettingsFrame server_settings;
-  server_settings.values[SETTINGS_H3_DATAGRAM] = 1;
+  server_settings.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1;
   server_settings.values[SETTINGS_WEBTRANS_DRAFT00] = 1;
   std::string data =
       std::string(1, kControlStream) + EncodeSettings(server_settings);
@@ -3474,7 +3533,7 @@
   CompleteHandshake();
 
   SettingsFrame server_settings;
-  server_settings.values[SETTINGS_H3_DATAGRAM] = 1;
+  server_settings.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1;
   server_settings.values[SETTINGS_WEBTRANS_DRAFT00] = 0;
   std::string data =
       std::string(1, kControlStream) + EncodeSettings(server_settings);
diff --git a/quic/core/http/quic_spdy_stream.cc b/quic/core/http/quic_spdy_stream.cc
index bc23474..810994e 100644
--- a/quic/core/http/quic_spdy_stream.cc
+++ b/quic/core/http/quic_spdy_stream.cc
@@ -15,6 +15,7 @@
 #include "absl/strings/string_view.h"
 #include "quic/core/http/http_constants.h"
 #include "quic/core/http/http_decoder.h"
+#include "quic/core/http/http_frames.h"
 #include "quic/core/http/quic_spdy_session.h"
 #include "quic/core/http/spdy_utils.h"
 #include "quic/core/http/web_transport_http3.h"
@@ -128,6 +129,10 @@
     return false;
   }
 
+  bool OnCapsuleFrame(const CapsuleFrame& frame) override {
+    return stream_->OnCapsuleFrame(frame);
+  }
+
   void OnWebTransportStreamFrameType(
       QuicByteCount header_length,
       WebTransportSessionId session_id) override {
@@ -287,6 +292,13 @@
     SetFinSent();
     CloseWriteSide();
   }
+
+  if (session()->perspective() == Perspective::IS_CLIENT && web_transport_) {
+    RegisterHttp3DatagramContextId(web_transport_->context_id(),
+                                   Http3DatagramContextExtensions(),
+                                   web_transport_.get());
+  }
+
   return bytes_written;
 }
 
@@ -1206,6 +1218,13 @@
       protocol = header_value;
     }
     if (header_name == "datagram-flow-id") {
+      if (spdy_session_->http_datagram_support() !=
+          HttpDatagramSupport::kDraft00) {
+        QUIC_DLOG(ERROR) << ENDPOINT
+                         << "Rejecting WebTransport due to unexpected "
+                            "Datagram-Flow-Id header";
+        return;
+      }
       if (flow_id.has_value() || header_value.empty()) {
         return;
       }
@@ -1217,16 +1236,26 @@
     }
   }
 
-  if (method != "CONNECT" || protocol != "webtransport" ||
-      !flow_id.has_value()) {
+  if (method != "CONNECT" || protocol != "webtransport") {
     return;
   }
 
-  RegisterHttp3DatagramFlowId(*flow_id);
+  if (spdy_session_->http_datagram_support() == HttpDatagramSupport::kDraft00) {
+    if (!flow_id.has_value()) {
+      QUIC_DLOG(ERROR)
+          << ENDPOINT
+          << "Rejecting WebTransport due to missing Datagram-Flow-Id header";
+      return;
+    }
+    RegisterHttp3DatagramFlowId(*flow_id);
+  }
 
   web_transport_ =
       std::make_unique<WebTransportHttp3>(spdy_session_, this, id());
 
+  if (spdy_session_->http_datagram_support() != HttpDatagramSupport::kDraft00) {
+    return;
+  }
   // If we're in draft-ietf-masque-h3-datagram-00 mode, pretend we also received
   // a REGISTER_DATAGRAM_NO_CONTEXT capsule with no extensions.
   // TODO(b/181256914) remove this when we remove support for
@@ -1255,14 +1284,12 @@
     return;
   }
 
-  QuicDatagramStreamId stream_id = id();
-  headers["datagram-flow-id"] = absl::StrCat(stream_id);
+  if (spdy_session_->http_datagram_support() == HttpDatagramSupport::kDraft00) {
+    headers["datagram-flow-id"] = absl::StrCat(id());
+  }
 
   web_transport_ =
       std::make_unique<WebTransportHttp3>(spdy_session_, this, id());
-  RegisterHttp3DatagramContextId(web_transport_->context_id(),
-                                 Http3DatagramContextExtensions(),
-                                 web_transport_.get());
 }
 
 void QuicSpdyStream::OnCanWriteNewData() {
@@ -1323,6 +1350,79 @@
     : session_id(session_id),
       adapter(stream->spdy_session_, stream, stream->sequencer()) {}
 
+bool QuicSpdyStream::OnCapsuleFrame(const CapsuleFrame& frame) {
+  QUIC_DLOG(INFO) << ENDPOINT << "Stream " << id() << " received capsule "
+                  << frame;
+  if (!headers_decompressed_) {
+    QUIC_PEER_BUG(capsule before headers)
+        << ENDPOINT << "Stream " << id() << " received capsule " << frame
+        << " before headers";
+    return false;
+  }
+  switch (frame.capsule_type) {
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      if (datagram_registration_visitor_ == nullptr) {
+        QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << frame
+                         << " without any registration visitor";
+        return false;
+      }
+      datagram_registration_visitor_->OnContextReceived(
+          id(), frame.register_datagram_context_capsule.context_id,
+          Http3DatagramContextExtensions());
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      if (datagram_registration_visitor_ == nullptr) {
+        QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << frame
+                         << " without any registration visitor";
+        return false;
+      }
+      datagram_registration_visitor_->OnContextClosed(
+          id(), frame.close_datagram_context_capsule.context_id,
+          Http3DatagramContextExtensions());
+      break;
+    case CapsuleType::DATAGRAM: {
+      Http3DatagramVisitor* visitor = nullptr;
+      if (frame.datagram_capsule.context_id.has_value()) {
+        auto it = datagram_context_visitors_.find(
+            frame.datagram_capsule.context_id.value());
+        if (it != datagram_context_visitors_.end()) {
+          visitor = it->second;
+        }
+      } else {
+        visitor = datagram_no_context_visitor_;
+      }
+      if (visitor == nullptr) {
+        QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << frame
+                         << " without any registration visitor";
+        return true;
+      }
+      visitor->OnHttp3Datagram(id(), frame.datagram_capsule.context_id,
+                               frame.datagram_capsule.http_datagram_payload);
+    } break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      if (datagram_registration_visitor_ == nullptr) {
+        QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << frame
+                         << " without any registration visitor";
+        return false;
+      }
+      datagram_registration_visitor_->OnContextReceived(
+          id(), /*context_id=*/absl::nullopt, Http3DatagramContextExtensions());
+      break;
+  }
+  return true;
+}
+
+void QuicSpdyStream::WriteCapsuleFrame(const CapsuleFrame& frame) {
+  QUIC_DLOG(INFO) << ENDPOINT << "Stream " << id() << " sending capsule "
+                  << frame;
+  std::unique_ptr<char[]> buffer;
+  QuicByteCount frame_length =
+      HttpEncoder::SerializeCapsuleFrame(frame, &buffer);
+  QUICHE_DCHECK_GT(frame_length, 0u);
+  WriteOrBufferData(absl::string_view(buffer.get(), frame_length),
+                    /*fin=*/false, /*ack_listener=*/nullptr);
+}
+
 MessageStatus QuicSpdyStream::SendHttp3Datagram(
     absl::optional<QuicDatagramContextId> context_id,
     absl::string_view payload) {
@@ -1368,19 +1468,22 @@
   if (visitor == nullptr) {
     QUIC_BUG(null datagram visitor)
         << ENDPOINT << "Null datagram visitor for stream ID " << id()
-        << " context ID " << (context_id.has_value() ? context_id.value() : 0);
+        << " context ID "
+        << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none");
     return;
   }
   if (datagram_registration_visitor_ == nullptr) {
     QUIC_BUG(context registration without registration visitor)
         << ENDPOINT << "Cannot register context ID "
-        << (context_id.has_value() ? context_id.value() : 0)
+        << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none")
         << " without registration visitor for stream ID " << id();
     return;
   }
   QUIC_DLOG(INFO) << ENDPOINT << "Registering datagram context ID "
-                  << (context_id.has_value() ? context_id.value() : 0)
+                  << (context_id.has_value() ? absl::StrCat(context_id.value())
+                                             : "none")
                   << " with stream ID " << id();
+
   if (context_id.has_value()) {
     if (datagram_no_context_visitor_ != nullptr) {
       QUIC_BUG(h3 datagram context ID mix1)
@@ -1392,28 +1495,45 @@
     }
     auto insertion_result =
         datagram_context_visitors_.insert({context_id.value(), visitor});
-    QUIC_BUG_IF(h3 datagram double context registration,
-                !insertion_result.second)
-        << ENDPOINT << "Attempted to doubly register HTTP/3 stream ID " << id()
-        << " context ID " << context_id.value();
-    return;
+    if (!insertion_result.second) {
+      QUIC_BUG(h3 datagram double context registration)
+          << ENDPOINT << "Attempted to doubly register HTTP/3 stream ID "
+          << id() << " context ID " << context_id.value();
+      return;
+    }
+  } else {
+    // Registration without a context ID.
+    if (!datagram_context_visitors_.empty()) {
+      QUIC_BUG(h3 datagram context ID mix2)
+          << ENDPOINT
+          << "Attempted to mix registrations with and without context IDs "
+             "for stream ID "
+          << id();
+      return;
+    }
+    if (datagram_no_context_visitor_ != nullptr) {
+      QUIC_BUG(h3 datagram double no context registration)
+          << ENDPOINT << "Attempted to doubly register HTTP/3 stream ID "
+          << id() << " with no context ID";
+      return;
+    }
+    datagram_no_context_visitor_ = visitor;
   }
-  // Registration without a context ID.
-  if (!datagram_context_visitors_.empty()) {
-    QUIC_BUG(h3 datagram context ID mix2)
-        << ENDPOINT
-        << "Attempted to mix registrations with and without context IDs "
-           "for stream ID "
-        << id();
-    return;
+  if (spdy_session_->http_datagram_support() == HttpDatagramSupport::kDraft03) {
+    const bool is_client = session()->perspective() == Perspective::IS_CLIENT;
+    if (context_id.has_value()) {
+      const bool is_client_context = context_id.value() % 2 == 0;
+      if (is_client == is_client_context) {
+        CapsuleFrame capsule_frame(CapsuleType::REGISTER_DATAGRAM_CONTEXT);
+        capsule_frame.register_datagram_context_capsule.context_id =
+            context_id.value();
+        WriteCapsuleFrame(capsule_frame);
+      }
+    } else if (is_client) {
+      WriteCapsuleFrame(
+          CapsuleFrame(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT));
+    }
   }
-  if (datagram_no_context_visitor_ != nullptr) {
-    QUIC_BUG(h3 datagram double no context registration)
-        << ENDPOINT << "Attempted to doubly register HTTP/3 stream ID " << id()
-        << " with no context ID";
-    return;
-  }
-  datagram_no_context_visitor_ = visitor;
 }
 
 void QuicSpdyStream::UnregisterHttp3DatagramContextId(
@@ -1421,26 +1541,34 @@
   if (datagram_registration_visitor_ == nullptr) {
     QUIC_BUG(context unregistration without registration visitor)
         << ENDPOINT << "Cannot unregister context ID "
-        << (context_id.has_value() ? context_id.value() : 0)
+        << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none")
         << " without registration visitor for stream ID " << id();
     return;
   }
   QUIC_DLOG(INFO) << ENDPOINT << "Unregistering datagram context ID "
-                  << (context_id.has_value() ? context_id.value() : 0)
+                  << (context_id.has_value() ? absl::StrCat(context_id.value())
+                                             : "none")
                   << " with stream ID " << id();
   if (context_id.has_value()) {
     size_t num_erased = datagram_context_visitors_.erase(context_id.value());
     QUIC_BUG_IF(h3 datagram unregister unknown context, num_erased != 1)
         << "Attempted to unregister unknown HTTP/3 context ID "
         << context_id.value() << " on stream ID " << id();
-    return;
+  } else {
+    // Unregistration without a context ID.
+    QUIC_BUG_IF(h3 datagram unknown context unregistration,
+                datagram_no_context_visitor_ == nullptr)
+        << "Attempted to unregister unknown no context on HTTP/3 stream ID "
+        << id();
+    datagram_no_context_visitor_ = nullptr;
   }
-  // Unregistration without a context ID.
-  QUIC_BUG_IF(h3 datagram unknown context unregistration,
-              datagram_no_context_visitor_ == nullptr)
-      << "Attempted to unregister unknown no context on HTTP/3 stream ID "
-      << id();
-  datagram_no_context_visitor_ = nullptr;
+  if (spdy_session_->http_datagram_support() == HttpDatagramSupport::kDraft03 &&
+      context_id.has_value()) {
+    CapsuleFrame capsule_frame(CapsuleType::CLOSE_DATAGRAM_CONTEXT);
+    capsule_frame.close_datagram_context_capsule.context_id =
+        context_id.value();
+    WriteCapsuleFrame(capsule_frame);
+  }
 }
 
 void QuicSpdyStream::MoveHttp3DatagramContextIdRegistration(
@@ -1449,12 +1577,13 @@
   if (datagram_registration_visitor_ == nullptr) {
     QUIC_BUG(context move without registration visitor)
         << ENDPOINT << "Cannot move context ID "
-        << (context_id.has_value() ? context_id.value() : 0)
+        << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none")
         << " without registration visitor for stream ID " << id();
     return;
   }
   QUIC_DLOG(INFO) << ENDPOINT << "Moving datagram context ID "
-                  << (context_id.has_value() ? context_id.value() : 0)
+                  << (context_id.has_value() ? absl::StrCat(context_id.value())
+                                             : "none")
                   << " with stream ID " << id();
   if (context_id.has_value()) {
     QUIC_BUG_IF(h3 datagram move unknown context,
diff --git a/quic/core/http/quic_spdy_stream.h b/quic/core/http/quic_spdy_stream.h
index 4df1196..d9a784d 100644
--- a/quic/core/http/quic_spdy_stream.h
+++ b/quic/core/http/quic_spdy_stream.h
@@ -21,6 +21,7 @@
 #include "absl/types/span.h"
 #include "quic/core/http/http_decoder.h"
 #include "quic/core/http/http_encoder.h"
+#include "quic/core/http/http_frames.h"
 #include "quic/core/http/quic_header_list.h"
 #include "quic/core/http/quic_spdy_stream_body_manager.h"
 #include "quic/core/qpack/qpack_decoded_headers_accumulator.h"
@@ -253,6 +254,8 @@
   // rejected due to buffer being full.  |write_size| must be non-zero.
   bool CanWriteNewBodyData(QuicByteCount write_size) const;
 
+  bool OnCapsuleFrame(const CapsuleFrame& frame);
+
   // Sends an HTTP/3 datagram. The stream and context IDs are not part of
   // |payload|.
   MessageStatus SendHttp3Datagram(
@@ -410,6 +413,8 @@
   ABSL_MUST_USE_RESULT bool WriteDataFrameHeader(QuicByteCount data_length,
                                                  bool force_write);
 
+  void WriteCapsuleFrame(const CapsuleFrame& frame);
+
   QuicSpdySession* spdy_session_;
 
   bool on_body_available_called_because_sequencer_is_closed_;
diff --git a/quic/core/http/quic_spdy_stream_test.cc b/quic/core/http/quic_spdy_stream_test.cc
index c19dc3c..50e4905 100644
--- a/quic/core/http/quic_spdy_stream_test.cc
+++ b/quic/core/http/quic_spdy_stream_test.cc
@@ -16,6 +16,7 @@
 #include "absl/strings/string_view.h"
 #include "quic/core/crypto/null_encrypter.h"
 #include "quic/core/http/http_encoder.h"
+#include "quic/core/http/quic_spdy_session.h"
 #include "quic/core/http/spdy_utils.h"
 #include "quic/core/http/web_transport_http3.h"
 #include "quic/core/quic_connection.h"
@@ -3017,7 +3018,7 @@
   }
 }
 
-TEST_P(QuicSpdyStreamTest, ProcessOutgoingWebTransportHeaders) {
+TEST_P(QuicSpdyStreamTest, ProcessOutgoingWebTransportHeadersDatagramDraft00) {
   if (!UsesHttp3()) {
     return;
   }
@@ -3025,7 +3026,9 @@
   InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
   session_->set_should_negotiate_h3_datagram(true);
   session_->EnableWebTransport();
-  QuicSpdySessionPeer::EnableWebTransport(*session_);
+  QuicSpdySessionPeer::EnableWebTransport(session_.get());
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft00);
 
   EXPECT_CALL(*stream_, WriteHeadersMock(false));
   EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _))
@@ -3040,7 +3043,31 @@
   EXPECT_EQ(stream_->id(), stream_->web_transport()->id());
 }
 
-TEST_P(QuicSpdyStreamTest, ProcessIncomingWebTransportHeaders) {
+TEST_P(QuicSpdyStreamTest, ProcessOutgoingWebTransportHeadersDatagramDraft03) {
+  if (!UsesHttp3()) {
+    return;
+  }
+
+  InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
+  session_->set_should_negotiate_h3_datagram(true);
+  session_->EnableWebTransport();
+  QuicSpdySessionPeer::EnableWebTransport(session_.get());
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft03);
+
+  EXPECT_CALL(*stream_, WriteHeadersMock(false));
+  EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _))
+      .Times(AnyNumber());
+
+  spdy::SpdyHeaderBlock headers;
+  headers[":method"] = "CONNECT";
+  headers[":protocol"] = "webtransport";
+  stream_->WriteHeaders(std::move(headers), /*fin=*/false, nullptr);
+  ASSERT_TRUE(stream_->web_transport() != nullptr);
+  EXPECT_EQ(stream_->id(), stream_->web_transport()->id());
+}
+
+TEST_P(QuicSpdyStreamTest, ProcessIncomingWebTransportHeadersDraft03) {
   if (!UsesHttp3()) {
     return;
   }
@@ -3048,7 +3075,36 @@
   Initialize(kShouldProcessData);
   session_->set_should_negotiate_h3_datagram(true);
   session_->EnableWebTransport();
-  QuicSpdySessionPeer::EnableWebTransport(*session_);
+  QuicSpdySessionPeer::EnableWebTransport(session_.get());
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft03);
+
+  headers_[":method"] = "CONNECT";
+  headers_[":protocol"] = "webtransport";
+
+  stream_->OnStreamHeadersPriority(
+      spdy::SpdyStreamPrecedence(kV3HighestPriority));
+  ProcessHeaders(false, headers_);
+  CapsuleFrame capsule_frame(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT);
+  stream_->OnCapsuleFrame(capsule_frame);
+  EXPECT_EQ("", stream_->data());
+  EXPECT_FALSE(stream_->header_list().empty());
+  EXPECT_FALSE(stream_->IsDoneReading());
+  ASSERT_TRUE(stream_->web_transport() != nullptr);
+  EXPECT_EQ(stream_->id(), stream_->web_transport()->id());
+}
+
+TEST_P(QuicSpdyStreamTest, ProcessIncomingWebTransportHeadersDraft00) {
+  if (!UsesHttp3()) {
+    return;
+  }
+
+  Initialize(kShouldProcessData);
+  session_->set_should_negotiate_h3_datagram(true);
+  session_->EnableWebTransport();
+  QuicSpdySessionPeer::EnableWebTransport(session_.get());
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft00);
 
   headers_[":method"] = "CONNECT";
   headers_[":protocol"] = "webtransport";
@@ -3075,7 +3131,9 @@
   Initialize(kShouldProcessData);
   session_->set_should_negotiate_h3_datagram(true);
   session_->EnableWebTransport();
-  QuicSpdySessionPeer::EnableWebTransport(*session_);
+  QuicSpdySessionPeer::EnableWebTransport(session_.get());
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft00);
 
   headers_[":method"] = "CONNECT";
   headers_[":protocol"] = "webtransport";
@@ -3119,13 +3177,14 @@
   stream_->UnregisterHttp3DatagramRegistrationVisitor();
 }
 
-TEST_P(QuicSpdyStreamTest, H3DatagramRegistrationWithoutContext) {
+TEST_P(QuicSpdyStreamTest, HttpDatagramRegistrationWithoutContextDraft00) {
   if (!UsesHttp3()) {
     return;
   }
-  Initialize(kShouldProcessData);
+  InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
   session_->set_should_negotiate_h3_datagram(true);
-  QuicSpdySessionPeer::SetH3DatagramSupported(session_.get(), true);
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft00);
   session_->RegisterHttp3DatagramFlowId(stream_->id(), stream_->id());
   ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor>
       h3_datagram_registration_visitor;
@@ -3168,23 +3227,39 @@
   session_->UnregisterHttp3DatagramFlowId(stream_->id());
 }
 
-TEST_P(QuicSpdyStreamTest, H3DatagramRegistrationWithContext) {
+TEST_P(QuicSpdyStreamTest, H3DatagramRegistrationWithoutContextDraft03) {
   if (!UsesHttp3()) {
     return;
   }
-  Initialize(kShouldProcessData);
+  InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
   session_->set_should_negotiate_h3_datagram(true);
-  QuicSpdySessionPeer::SetH3DatagramSupported(session_.get(), true);
-  session_->RegisterHttp3DatagramFlowId(stream_->id(), stream_->id());
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft03);
   ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor>
       h3_datagram_registration_visitor;
   SavingHttp3DatagramVisitor h3_datagram_visitor;
-  absl::optional<QuicDatagramContextId> context_id = 42;
+  absl::optional<QuicDatagramContextId> context_id;
   Http3DatagramContextExtensions extensions;
+  ASSERT_EQ(QuicDataWriter::GetVarInt62Len(stream_->id()), 1);
+  std::array<char, 256> datagram;
+  datagram[0] = stream_->id();
+  for (size_t i = 1; i < datagram.size(); i++) {
+    datagram[i] = i;
+  }
   stream_->RegisterHttp3DatagramRegistrationVisitor(
       &h3_datagram_registration_visitor);
+
+  // Expect us to send a REGISTER_DATAGRAM_NO_CONTEXT capsule.
+  EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _));
+
   stream_->RegisterHttp3DatagramContextId(context_id, extensions,
                                           &h3_datagram_visitor);
+  session_->OnMessageReceived(
+      absl::string_view(datagram.data(), datagram.size()));
+  EXPECT_THAT(h3_datagram_visitor.received_h3_datagrams(),
+              ElementsAre(SavingHttp3DatagramVisitor::SavedHttp3Datagram{
+                  stream_->id(), context_id,
+                  std::string(&datagram[1], datagram.size() - 1)}));
   // Test move.
   ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor>
       h3_datagram_registration_visitor2;
@@ -3192,9 +3267,72 @@
   SavingHttp3DatagramVisitor h3_datagram_visitor2;
   stream_->MoveHttp3DatagramContextIdRegistration(context_id,
                                                   &h3_datagram_visitor2);
+  EXPECT_TRUE(h3_datagram_visitor2.received_h3_datagrams().empty());
+  session_->OnMessageReceived(
+      absl::string_view(datagram.data(), datagram.size()));
+  EXPECT_THAT(h3_datagram_visitor2.received_h3_datagrams(),
+              ElementsAre(SavingHttp3DatagramVisitor::SavedHttp3Datagram{
+                  stream_->id(), context_id,
+                  std::string(&datagram[1], datagram.size() - 1)}));
   // Cleanup.
   stream_->UnregisterHttp3DatagramContextId(context_id);
   stream_->UnregisterHttp3DatagramRegistrationVisitor();
+}
+
+TEST_P(QuicSpdyStreamTest, HttpDatagramRegistrationWithContext) {
+  if (!UsesHttp3()) {
+    return;
+  }
+  InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
+  session_->set_should_negotiate_h3_datagram(true);
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft03);
+  ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor>
+      h3_datagram_registration_visitor;
+  SavingHttp3DatagramVisitor h3_datagram_visitor;
+  absl::optional<QuicDatagramContextId> context_id = 42;
+  Http3DatagramContextExtensions extensions;
+  ASSERT_EQ(QuicDataWriter::GetVarInt62Len(stream_->id()), 1);
+  std::array<char, 256> datagram;
+  datagram[0] = stream_->id();
+  datagram[1] = context_id.value();
+  for (size_t i = 2; i < datagram.size(); i++) {
+    datagram[i] = i;
+  }
+  stream_->RegisterHttp3DatagramRegistrationVisitor(
+      &h3_datagram_registration_visitor);
+
+  // Expect us to send a REGISTER_DATAGRAM_CONTEXT capsule.
+  EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _));
+
+  stream_->RegisterHttp3DatagramContextId(context_id, extensions,
+                                          &h3_datagram_visitor);
+  session_->OnMessageReceived(
+      absl::string_view(datagram.data(), datagram.size()));
+  EXPECT_THAT(h3_datagram_visitor.received_h3_datagrams(),
+              ElementsAre(SavingHttp3DatagramVisitor::SavedHttp3Datagram{
+                  stream_->id(), context_id,
+                  std::string(&datagram[2], datagram.size() - 2)}));
+  // Test move.
+  ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor>
+      h3_datagram_registration_visitor2;
+  stream_->MoveHttp3DatagramRegistration(&h3_datagram_registration_visitor2);
+  SavingHttp3DatagramVisitor h3_datagram_visitor2;
+  stream_->MoveHttp3DatagramContextIdRegistration(context_id,
+                                                  &h3_datagram_visitor2);
+  EXPECT_TRUE(h3_datagram_visitor2.received_h3_datagrams().empty());
+  session_->OnMessageReceived(
+      absl::string_view(datagram.data(), datagram.size()));
+  EXPECT_THAT(h3_datagram_visitor2.received_h3_datagrams(),
+              ElementsAre(SavingHttp3DatagramVisitor::SavedHttp3Datagram{
+                  stream_->id(), context_id,
+                  std::string(&datagram[2], datagram.size() - 2)}));
+  // Cleanup.
+
+  // Expect us to send a CLOSE_DATAGRAM_CONTEXT capsule.
+  EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _));
+  stream_->UnregisterHttp3DatagramContextId(context_id);
+  stream_->UnregisterHttp3DatagramRegistrationVisitor();
   session_->UnregisterHttp3DatagramFlowId(stream_->id());
 }
 
@@ -3204,7 +3342,8 @@
   }
   Initialize(kShouldProcessData);
   session_->set_should_negotiate_h3_datagram(true);
-  QuicSpdySessionPeer::SetH3DatagramSupported(session_.get(), true);
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft03);
   absl::optional<QuicDatagramContextId> context_id;
   std::string h3_datagram_payload = {1, 2, 3, 4, 5, 6};
   EXPECT_CALL(*connection_, SendMessage(1, _, false))
diff --git a/quic/core/quic_constants.h b/quic/core/quic_constants.h
index 162de89..97f7567 100644
--- a/quic/core/quic_constants.h
+++ b/quic/core/quic_constants.h
@@ -301,6 +301,10 @@
   kDatagramContextIdIncrement = 2,
 };
 
+enum : uint64_t {
+  kHttpDatagramStreamIdDivisor = 4,
+};
+
 }  // namespace quic
 
 #endif  // QUICHE_QUIC_CORE_QUIC_CONSTANTS_H_
diff --git a/quic/masque/masque_client_session.cc b/quic/masque/masque_client_session.cc
index 0d86569..f3c70e2 100644
--- a/quic/masque/masque_client_session.cc
+++ b/quic/masque/masque_client_session.cc
@@ -3,7 +3,9 @@
 // found in the LICENSE file.
 
 #include "quic/masque/masque_client_session.h"
+
 #include "absl/algorithm/container.h"
+#include "absl/strings/str_cat.h"
 #include "quic/core/http/spdy_utils.h"
 #include "quic/core/quic_data_reader.h"
 #include "quic/core/quic_utils.h"
@@ -101,7 +103,9 @@
   headers[":scheme"] = "masque";
   headers[":path"] = "/";
   headers[":authority"] = target_server_address.ToString();
-  SpdyUtils::AddDatagramFlowIdHeader(&headers, stream->id());
+  if (http_datagram_support() == HttpDatagramSupport::kDraft00) {
+    SpdyUtils::AddDatagramFlowIdHeader(&headers, stream->id());
+  }
   size_t bytes_sent =
       stream->SendRequest(std::move(headers), /*body=*/"", /*fin=*/false);
   if (bytes_sent == 0) {
@@ -142,8 +146,8 @@
                 << " compressed with stream ID " << connect_udp->stream()->id()
                 << " context ID "
                 << (connect_udp->context_id().has_value()
-                        ? connect_udp->context_id().value()
-                        : 0)
+                        ? absl::StrCat(connect_udp->context_id().value())
+                        : "none")
                 << " and got message status "
                 << MessageStatusToString(message_status);
 }
@@ -182,8 +186,8 @@
       QUIC_DLOG(INFO) << "Removing state for stream ID " << it->stream()->id()
                       << " context ID "
                       << (it->context_id().has_value()
-                              ? it->context_id().value()
-                              : 0);
+                              ? absl::StrCat(it->context_id().value())
+                              : "none");
       auto* stream = it->stream();
       it = connect_udp_client_states_.erase(it);
       if (!stream->write_side_closed()) {
@@ -224,8 +228,8 @@
       QUIC_DLOG(INFO) << "Stream " << stream_id
                       << " was closed, removing state for context ID "
                       << (it->context_id().has_value()
-                              ? it->context_id().value()
-                              : 0);
+                              ? absl::StrCat(it->context_id().value())
+                              : "none");
       auto* encapsulated_client_session = it->encapsulated_client_session();
       it = connect_udp_client_states_.erase(it);
       encapsulated_client_session->CloseConnection(
@@ -241,6 +245,7 @@
 }
 
 bool MasqueClientSession::OnSettingsFrame(const SettingsFrame& frame) {
+  QUIC_DLOG(INFO) << "Received SETTINGS: " << frame;
   if (!QuicSpdyClientSession::OnSettingsFrame(frame)) {
     QUIC_DLOG(ERROR) << "Failed to parse received settings";
     return false;
@@ -249,6 +254,7 @@
     QUIC_DLOG(ERROR) << "Refusing to use MASQUE without HTTP/3 Datagrams";
     return false;
   }
+  QUIC_DLOG(INFO) << "Using HTTP Datagram: " << http_datagram_support();
   owner_->OnSettingsReceived();
   return true;
 }
@@ -308,7 +314,8 @@
   QUIC_DVLOG(1) << "Sent " << payload.size()
                 << " bytes to connection for stream ID " << stream_id
                 << " context ID "
-                << (context_id.has_value() ? context_id.value() : 0);
+                << (context_id.has_value() ? absl::StrCat(context_id.value())
+                                           : "none");
 }
 
 void MasqueClientSession::ConnectUdpClientState::OnContextReceived(
@@ -321,11 +328,13 @@
     return;
   }
   if (context_id != context_id_) {
-    QUIC_DLOG(INFO) << "Ignoring unexpected context ID "
-                    << (context_id.has_value() ? context_id.value() : 0)
-                    << " instead of "
-                    << (context_id_.has_value() ? context_id_.value() : 0)
-                    << " on stream ID " << stream_->id();
+    QUIC_DLOG(INFO)
+        << "Ignoring unexpected context ID "
+        << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none")
+        << " instead of "
+        << (context_id_.has_value() ? absl::StrCat(context_id_.value())
+                                    : "none")
+        << " on stream ID " << stream_->id();
     return;
   }
   // Do nothing since the client registers first and we currently ignore
@@ -342,11 +351,13 @@
     return;
   }
   if (context_id != context_id_) {
-    QUIC_DLOG(INFO) << "Ignoring unexpected close of context ID "
-                    << (context_id.has_value() ? context_id.value() : 0)
-                    << " instead of "
-                    << (context_id_.has_value() ? context_id_.value() : 0)
-                    << " on stream ID " << stream_->id();
+    QUIC_DLOG(INFO)
+        << "Ignoring unexpected close of context ID "
+        << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none")
+        << " instead of "
+        << (context_id_.has_value() ? absl::StrCat(context_id_.value())
+                                    : "none")
+        << " on stream ID " << stream_->id();
     return;
   }
   QUIC_DLOG(INFO) << "Received datagram context close on stream ID "
diff --git a/quic/masque/masque_server_session.cc b/quic/masque/masque_server_session.cc
index effc17f..a38ae9d 100644
--- a/quic/masque/masque_server_session.cc
+++ b/quic/masque/masque_server_session.cc
@@ -202,13 +202,15 @@
       QUIC_DLOG(ERROR) << "MASQUE request with bad method \"" << method << "\"";
       return CreateBackendErrorResponse("400", "Bad method");
     }
-    absl::optional<QuicDatagramStreamId> flow_id =
-        SpdyUtils::ParseDatagramFlowIdHeader(request_headers);
-    if (!flow_id.has_value()) {
-      QUIC_DLOG(ERROR)
-          << "MASQUE request with bad or missing DatagramFlowId header";
-      return CreateBackendErrorResponse("400",
-                                        "Bad or missing DatagramFlowId header");
+    absl::optional<QuicDatagramStreamId> flow_id;
+    if (http_datagram_support() == HttpDatagramSupport::kDraft00) {
+      flow_id = SpdyUtils::ParseDatagramFlowIdHeader(request_headers);
+      if (!flow_id.has_value()) {
+        QUIC_DLOG(ERROR)
+            << "MASQUE request with bad or missing DatagramFlowId header";
+        return CreateBackendErrorResponse(
+            "400", "Bad or missing DatagramFlowId header");
+      }
     }
     QuicUrl url(absl::StrCat("https://", authority));
     if (!url.IsValid() || url.PathParamsQuery() != "/") {
@@ -235,7 +237,9 @@
         info_list, freeaddrinfo);
     QuicSocketAddress target_server_address(info_list->ai_addr,
                                             info_list->ai_addrlen);
-    QUIC_DLOG(INFO) << "Got CONNECT_UDP request flow_id=" << *flow_id
+    QUIC_DLOG(INFO) << "Got CONNECT_UDP request on stream ID "
+                    << request_handler->stream_id() << " flow_id="
+                    << (flow_id.has_value() ? absl::StrCat(*flow_id) : "none")
                     << " target_server_address=\"" << target_server_address
                     << "\"";
 
@@ -264,20 +268,26 @@
           << request_handler->stream_id();
       return CreateBackendErrorResponse("500", "Bad stream type");
     }
-    stream->RegisterHttp3DatagramFlowId(*flow_id);
+    if (flow_id.has_value()) {
+      stream->RegisterHttp3DatagramFlowId(*flow_id);
+    }
     connect_udp_server_states_.push_back(
         ConnectUdpServerState(stream, context_id, target_server_address,
                               fd_wrapper.extract_fd(), this));
 
-    // TODO(b/181256914) remove this when we drop support for
-    // draft-ietf-masque-h3-datagram-00 in favor of later drafts.
-    Http3DatagramContextExtensions extensions;
-    stream->RegisterHttp3DatagramContextId(context_id, extensions,
-                                           &connect_udp_server_states_.back());
+    if (http_datagram_support() == HttpDatagramSupport::kDraft00) {
+      // TODO(b/181256914) remove this when we drop support for
+      // draft-ietf-masque-h3-datagram-00 in favor of later drafts.
+      Http3DatagramContextExtensions extensions;
+      stream->RegisterHttp3DatagramContextId(
+          context_id, extensions, &connect_udp_server_states_.back());
+    }
 
     spdy::Http2HeaderBlock response_headers;
     response_headers[":status"] = "200";
-    SpdyUtils::AddDatagramFlowIdHeader(&response_headers, *flow_id);
+    if (flow_id.has_value()) {
+      SpdyUtils::AddDatagramFlowIdHeader(&response_headers, *flow_id);
+    }
     auto response = std::make_unique<QuicBackendResponse>();
     response->set_response_type(QuicBackendResponse::INCOMPLETE_RESPONSE);
     response->set_headers(std::move(response_headers));
@@ -424,6 +434,19 @@
   return std::string("MasqueServerSession-") + connection_id().ToString();
 }
 
+bool MasqueServerSession::OnSettingsFrame(const SettingsFrame& frame) {
+  QUIC_DLOG(INFO) << "Received SETTINGS: " << frame;
+  if (!QuicSimpleServerSession::OnSettingsFrame(frame)) {
+    return false;
+  }
+  if (!SupportsH3Datagram()) {
+    QUIC_DLOG(ERROR) << "Refusing to use MASQUE without HTTP Datagrams";
+    return false;
+  }
+  QUIC_DLOG(INFO) << "Using HTTP Datagram: " << http_datagram_support();
+  return true;
+}
+
 MasqueServerSession::ConnectUdpServerState::ConnectUdpServerState(
     QuicSpdyStream* stream, absl::optional<QuicDatagramContextId> context_id,
     const QuicSocketAddress& target_server_address, QuicUdpSocketFd fd,
@@ -440,10 +463,10 @@
 
 MasqueServerSession::ConnectUdpServerState::~ConnectUdpServerState() {
   if (stream() != nullptr) {
-    stream()->UnregisterHttp3DatagramRegistrationVisitor();
     if (context_registered_) {
       stream()->UnregisterHttp3DatagramContextId(context_id());
     }
+    stream()->UnregisterHttp3DatagramRegistrationVisitor();
   }
   if (fd_ == kQuicInvalidSocketFd) {
     return;
@@ -515,17 +538,20 @@
     context_id_ = context_id;
   }
   if (context_id != context_id_) {
-    QUIC_DLOG(INFO) << "Ignoring unexpected context ID "
-                    << (context_id.has_value() ? context_id.value() : 0)
-                    << " instead of "
-                    << (context_id_.has_value() ? context_id_.value() : 0)
-                    << " on stream ID " << stream()->id();
+    QUIC_DLOG(INFO)
+        << "Ignoring unexpected context ID "
+        << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none")
+        << " instead of "
+        << (context_id_.has_value() ? absl::StrCat(context_id_.value())
+                                    : "none")
+        << " on stream ID " << stream()->id();
     return;
   }
   if (context_registered_) {
     QUIC_BUG(MASQUE server double datagram context registration)
         << "Try to re-register stream ID " << stream_id << " context ID "
-        << (context_id_.has_value() ? context_id_.value() : 0);
+        << (context_id_.has_value() ? absl::StrCat(context_id_.value())
+                                    : "none");
     return;
   }
   context_registered_ = true;
@@ -543,11 +569,13 @@
     return;
   }
   if (context_id != context_id_) {
-    QUIC_DLOG(INFO) << "Ignoring unexpected close of context ID "
-                    << (context_id.has_value() ? context_id.value() : 0)
-                    << " instead of "
-                    << (context_id_.has_value() ? context_id_.value() : 0)
-                    << " on stream ID " << stream()->id();
+    QUIC_DLOG(INFO)
+        << "Ignoring unexpected close of context ID "
+        << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none")
+        << " instead of "
+        << (context_id_.has_value() ? absl::StrCat(context_id_.value())
+                                    : "none")
+        << " on stream ID " << stream()->id();
     return;
   }
   QUIC_DLOG(INFO) << "Received datagram context close on stream ID "
diff --git a/quic/masque/masque_server_session.h b/quic/masque/masque_server_session.h
index 1bec14c..5729655 100644
--- a/quic/masque/masque_server_session.h
+++ b/quic/masque/masque_server_session.h
@@ -136,6 +136,8 @@
     bool context_registered_ = false;
   };
 
+  // From QuicSpdySession.
+  bool OnSettingsFrame(const SettingsFrame& frame) override;
   bool ShouldNegotiateHttp3Datagram() override { return true; }
 
   MasqueServerBackend* masque_server_backend_;  // Unowned.
diff --git a/quic/test_tools/quic_spdy_session_peer.cc b/quic/test_tools/quic_spdy_session_peer.cc
index a27f073..d0fd669 100644
--- a/quic/test_tools/quic_spdy_session_peer.cc
+++ b/quic/test_tools/quic_spdy_session_peer.cc
@@ -100,9 +100,9 @@
 }
 
 // static
-void QuicSpdySessionPeer::SetH3DatagramSupported(QuicSpdySession* session,
-                                                 bool h3_datagram_supported) {
-  session->h3_datagram_supported_ = h3_datagram_supported;
+void QuicSpdySessionPeer::SetHttpDatagramSupport(
+    QuicSpdySession* session, HttpDatagramSupport http_datagram_support) {
+  session->http_datagram_support_ = http_datagram_support;
 }
 
 // static
@@ -112,10 +112,10 @@
 }
 
 // static
-void QuicSpdySessionPeer::EnableWebTransport(QuicSpdySession& session) {
-  QUICHE_DCHECK(session.WillNegotiateWebTransport());
-  session.h3_datagram_supported_ = true;
-  session.peer_supports_webtransport_ = true;
+void QuicSpdySessionPeer::EnableWebTransport(QuicSpdySession* session) {
+  QUICHE_DCHECK(session->WillNegotiateWebTransport());
+  SetHttpDatagramSupport(session, HttpDatagramSupport::kDraft03);
+  session->peer_supports_webtransport_ = true;
 }
 
 }  // namespace test
diff --git a/quic/test_tools/quic_spdy_session_peer.h b/quic/test_tools/quic_spdy_session_peer.h
index 0d06c4d..2aa204b 100644
--- a/quic/test_tools/quic_spdy_session_peer.h
+++ b/quic/test_tools/quic_spdy_session_peer.h
@@ -7,6 +7,7 @@
 
 #include "quic/core/http/quic_receive_control_stream.h"
 #include "quic/core/http/quic_send_control_stream.h"
+#include "quic/core/http/quic_spdy_session.h"
 #include "quic/core/qpack/qpack_receive_stream.h"
 #include "quic/core/qpack/qpack_send_stream.h"
 #include "quic/core/quic_packets.h"
@@ -16,7 +17,6 @@
 namespace quic {
 
 class QuicHeadersStream;
-class QuicSpdySession;
 
 namespace test {
 
@@ -50,10 +50,10 @@
       QuicSpdySession* session);
   static QpackReceiveStream* GetQpackEncoderReceiveStream(
       QuicSpdySession* session);
-  static void SetH3DatagramSupported(QuicSpdySession* session,
-                                     bool h3_datagram_supported);
+  static void SetHttpDatagramSupport(QuicSpdySession* session,
+                                     HttpDatagramSupport http_datagram_support);
   static bool ShouldNegotiateHttp3Datagram(QuicSpdySession* session);
-  static void EnableWebTransport(QuicSpdySession& session);
+  static void EnableWebTransport(QuicSpdySession* session);
 };
 
 }  // namespace test