Support latest HTTP Datagrams draft

This CL updates our code to support draft-ietf-masque-h3-datagram-04, and the corresponding changes in draft-ietf-masque-connect-udp and draft-ietf-webtrans-http3.

These features are disabled on our servers, so they do not require flag protection.

PiperOrigin-RevId: 399795495
diff --git a/quic/core/http/capsule.cc b/quic/core/http/capsule.cc
new file mode 100644
index 0000000..0f9eefc
--- /dev/null
+++ b/quic/core/http/capsule.cc
@@ -0,0 +1,558 @@
+// Copyright (c) 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quic/core/http/capsule.h"
+
+#include <type_traits>
+
+#include "absl/strings/escaping.h"
+#include "absl/strings/string_view.h"
+#include "quic/core/http/http_frames.h"
+#include "quic/core/quic_data_reader.h"
+#include "quic/core/quic_data_writer.h"
+#include "quic/platform/api/quic_bug_tracker.h"
+#include "common/platform/api/quiche_logging.h"
+
+namespace quic {
+
+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), ")");
+}
+
+std::ostream& operator<<(std::ostream& os, const CapsuleType& capsule_type) {
+  os << CapsuleTypeToString(capsule_type);
+  return os;
+}
+
+std::string DatagramFormatTypeToString(
+    DatagramFormatType datagram_format_type) {
+  switch (datagram_format_type) {
+    case DatagramFormatType::UDP_PAYLOAD:
+      return "UDP_PAYLOAD";
+    case DatagramFormatType::WEBTRANSPORT:
+      return "WEBTRANSPORT";
+  }
+  return absl::StrCat("Unknown(", static_cast<uint64_t>(datagram_format_type),
+                      ")");
+}
+
+std::ostream& operator<<(std::ostream& os,
+                         const DatagramFormatType& datagram_format_type) {
+  os << DatagramFormatTypeToString(datagram_format_type);
+  return os;
+}
+
+std::string ContextCloseCodeToString(ContextCloseCode context_close_code) {
+  switch (context_close_code) {
+    case ContextCloseCode::NO_ERROR:
+      return "NO_ERROR";
+    case ContextCloseCode::UNKNOWN_FORMAT:
+      return "UNKNOWN_FORMAT";
+    case ContextCloseCode::DENIED:
+      return "DENIED";
+    case ContextCloseCode::RESOURCE_LIMIT:
+      return "RESOURCE_LIMIT";
+  }
+  return absl::StrCat("Unknown(", static_cast<uint64_t>(context_close_code),
+                      ")");
+}
+
+std::ostream& operator<<(std::ostream& os,
+                         const ContextCloseCode& context_close_code) {
+  os << ContextCloseCodeToString(context_close_code);
+  return os;
+}
+
+Capsule::Capsule(CapsuleType capsule_type) : capsule_type_(capsule_type) {
+  switch (capsule_type) {
+    case CapsuleType::DATAGRAM:
+      static_assert(std::is_standard_layout<DatagramCapsule>::value &&
+                        std::is_trivially_destructible<DatagramCapsule>::value,
+                    "All capsule structs must have these properties");
+      datagram_capsule_ = DatagramCapsule();
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      static_assert(
+          std::is_standard_layout<RegisterDatagramContextCapsule>::value &&
+              std::is_trivially_destructible<
+                  RegisterDatagramContextCapsule>::value,
+          "All capsule structs must have these properties");
+      register_datagram_context_capsule_ = RegisterDatagramContextCapsule();
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      static_assert(
+          std::is_standard_layout<RegisterDatagramNoContextCapsule>::value &&
+              std::is_trivially_destructible<
+                  RegisterDatagramNoContextCapsule>::value,
+          "All capsule structs must have these properties");
+      register_datagram_no_context_capsule_ =
+          RegisterDatagramNoContextCapsule();
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      static_assert(
+          std::is_standard_layout<CloseDatagramContextCapsule>::value &&
+              std::is_trivially_destructible<
+                  CloseDatagramContextCapsule>::value,
+          "All capsule structs must have these properties");
+      close_datagram_context_capsule_ = CloseDatagramContextCapsule();
+      break;
+    default:
+      unknown_capsule_data_ = absl::string_view();
+      break;
+  }
+}
+
+// static
+Capsule Capsule::Datagram(absl::optional<QuicDatagramContextId> context_id,
+                          absl::string_view http_datagram_payload) {
+  Capsule capsule(CapsuleType::DATAGRAM);
+  capsule.datagram_capsule().context_id = context_id;
+  capsule.datagram_capsule().http_datagram_payload = http_datagram_payload;
+  return capsule;
+}
+
+// static
+Capsule Capsule::RegisterDatagramContext(
+    QuicDatagramContextId context_id, DatagramFormatType format_type,
+    absl::string_view format_additional_data) {
+  Capsule capsule(CapsuleType::REGISTER_DATAGRAM_CONTEXT);
+  capsule.register_datagram_context_capsule().context_id = context_id;
+  capsule.register_datagram_context_capsule().format_type = format_type;
+  capsule.register_datagram_context_capsule().format_additional_data =
+      format_additional_data;
+  return capsule;
+}
+
+// static
+Capsule Capsule::RegisterDatagramNoContext(
+    DatagramFormatType format_type, absl::string_view format_additional_data) {
+  Capsule capsule(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT);
+  capsule.register_datagram_no_context_capsule().format_type = format_type;
+  capsule.register_datagram_no_context_capsule().format_additional_data =
+      format_additional_data;
+  return capsule;
+}
+
+// static
+Capsule Capsule::CloseDatagramContext(QuicDatagramContextId context_id,
+                                      ContextCloseCode close_code,
+                                      absl::string_view close_details) {
+  Capsule capsule(CapsuleType::CLOSE_DATAGRAM_CONTEXT);
+  capsule.close_datagram_context_capsule().context_id = context_id;
+  capsule.close_datagram_context_capsule().close_code = close_code;
+  capsule.close_datagram_context_capsule().close_details = close_details;
+  return capsule;
+}
+
+// static
+Capsule Capsule::Unknown(uint64_t capsule_type,
+                         absl::string_view unknown_capsule_data) {
+  Capsule capsule(static_cast<CapsuleType>(capsule_type));
+  capsule.unknown_capsule_data() = unknown_capsule_data;
+  return capsule;
+}
+
+Capsule& Capsule::operator=(const Capsule& other) {
+  capsule_type_ = other.capsule_type_;
+  switch (capsule_type_) {
+    case CapsuleType::DATAGRAM:
+      datagram_capsule_ = other.datagram_capsule_;
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      register_datagram_context_capsule_ =
+          other.register_datagram_context_capsule_;
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      register_datagram_no_context_capsule_ =
+          other.register_datagram_no_context_capsule_;
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      close_datagram_context_capsule_ = other.close_datagram_context_capsule_;
+      break;
+    default:
+      unknown_capsule_data_ = other.unknown_capsule_data_;
+      break;
+  }
+  return *this;
+}
+
+Capsule::Capsule(const Capsule& other) : Capsule(other.capsule_type_) {
+  *this = other;
+}
+
+bool Capsule::operator==(const Capsule& other) const {
+  if (capsule_type_ != other.capsule_type_) {
+    return false;
+  }
+  switch (capsule_type_) {
+    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_CONTEXT:
+      return register_datagram_context_capsule_.context_id ==
+                 other.register_datagram_context_capsule_.context_id &&
+             register_datagram_context_capsule_.format_type ==
+                 other.register_datagram_context_capsule_.format_type &&
+             register_datagram_context_capsule_.format_additional_data ==
+                 other.register_datagram_context_capsule_
+                     .format_additional_data;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      return register_datagram_no_context_capsule_.format_type ==
+                 other.register_datagram_no_context_capsule_.format_type &&
+             register_datagram_no_context_capsule_.format_additional_data ==
+                 other.register_datagram_no_context_capsule_
+                     .format_additional_data;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      return close_datagram_context_capsule_.context_id ==
+                 other.close_datagram_context_capsule_.context_id &&
+             close_datagram_context_capsule_.close_code ==
+                 other.close_datagram_context_capsule_.close_code &&
+             close_datagram_context_capsule_.close_details ==
+                 other.close_datagram_context_capsule_.close_details;
+    default:
+      return unknown_capsule_data_ == other.unknown_capsule_data_;
+  }
+}
+
+std::string Capsule::ToString() const {
+  std::string rv = CapsuleTypeToString(capsule_type_);
+  switch (capsule_type_) {
+    case CapsuleType::DATAGRAM:
+      if (datagram_capsule_.context_id.has_value()) {
+        absl::StrAppend(&rv, "(", datagram_capsule_.context_id.value(), ")");
+      }
+      absl::StrAppend(
+          &rv, "[",
+          absl::BytesToHexString(datagram_capsule_.http_datagram_payload), "]");
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      absl::StrAppend(
+          &rv, "(context_id=", register_datagram_context_capsule_.context_id,
+          ",format_type=",
+          DatagramFormatTypeToString(
+              register_datagram_context_capsule_.format_type),
+          "){",
+          absl::BytesToHexString(
+              register_datagram_context_capsule_.format_additional_data),
+          "}");
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      absl::StrAppend(
+          &rv, "(format_type=",
+          DatagramFormatTypeToString(
+              register_datagram_no_context_capsule_.format_type),
+          "){",
+          absl::BytesToHexString(
+              register_datagram_no_context_capsule_.format_additional_data),
+          "}");
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      absl::StrAppend(
+          &rv, "(context_id=", close_datagram_context_capsule_.context_id,
+          ",close_code=",
+          ContextCloseCodeToString(close_datagram_context_capsule_.close_code),
+          ",close_details=\"",
+          absl::BytesToHexString(close_datagram_context_capsule_.close_details),
+          "\")");
+      break;
+    default:
+      absl::StrAppend(&rv, "[", absl::BytesToHexString(unknown_capsule_data_),
+                      "]");
+      break;
+  }
+  return rv;
+}
+
+std::ostream& operator<<(std::ostream& os, const Capsule& capsule) {
+  os << capsule.ToString();
+  return os;
+}
+
+CapsuleParser::CapsuleParser(Visitor* visitor) : visitor_(visitor) {
+  QUICHE_DCHECK_NE(visitor_, nullptr);
+}
+
+QuicBuffer SerializeCapsule(const Capsule& capsule,
+                            QuicBufferAllocator* allocator) {
+  QuicByteCount capsule_type_length = QuicDataWriter::GetVarInt62Len(
+      static_cast<uint64_t>(capsule.capsule_type()));
+  QuicByteCount capsule_data_length;
+  switch (capsule.capsule_type()) {
+    case CapsuleType::DATAGRAM:
+      capsule_data_length =
+          capsule.datagram_capsule().http_datagram_payload.length();
+      if (capsule.datagram_capsule().context_id.has_value()) {
+        capsule_data_length += QuicDataWriter::GetVarInt62Len(
+            capsule.datagram_capsule().context_id.value());
+      }
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      capsule_data_length =
+          QuicDataWriter::GetVarInt62Len(
+              capsule.register_datagram_context_capsule().context_id) +
+          QuicDataWriter::GetVarInt62Len(static_cast<uint64_t>(
+              capsule.register_datagram_context_capsule().format_type)) +
+          capsule.register_datagram_context_capsule()
+              .format_additional_data.length();
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      capsule_data_length =
+          QuicDataWriter::GetVarInt62Len(static_cast<uint64_t>(
+              capsule.register_datagram_no_context_capsule().format_type)) +
+          capsule.register_datagram_no_context_capsule()
+              .format_additional_data.length();
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      capsule_data_length =
+          QuicDataWriter::GetVarInt62Len(
+              capsule.close_datagram_context_capsule().context_id) +
+          QuicDataWriter::GetVarInt62Len(static_cast<uint64_t>(
+              capsule.close_datagram_context_capsule().close_code)) +
+          capsule.close_datagram_context_capsule().close_details.length();
+      break;
+    default:
+      capsule_data_length = capsule.unknown_capsule_data().length();
+      break;
+  }
+  QuicByteCount capsule_length_length =
+      QuicDataWriter::GetVarInt62Len(capsule_data_length);
+  QuicByteCount total_capsule_length =
+      capsule_type_length + capsule_length_length + capsule_data_length;
+  QuicBuffer buffer(allocator, total_capsule_length);
+  QuicDataWriter writer(buffer.size(), buffer.data());
+  if (!writer.WriteVarInt62(static_cast<uint64_t>(capsule.capsule_type()))) {
+    QUIC_BUG(capsule type write fail) << "Failed to write CAPSULE type";
+    return QuicBuffer();
+  }
+  if (!writer.WriteVarInt62(capsule_data_length)) {
+    QUIC_BUG(capsule length write fail) << "Failed to write CAPSULE length";
+    return QuicBuffer();
+  }
+  switch (capsule.capsule_type()) {
+    case CapsuleType::DATAGRAM:
+      if (capsule.datagram_capsule().context_id.has_value()) {
+        if (!writer.WriteVarInt62(
+                capsule.datagram_capsule().context_id.value())) {
+          QUIC_BUG(datagram capsule context ID write fail)
+              << "Failed to write DATAGRAM CAPSULE context ID";
+          return QuicBuffer();
+        }
+      }
+      if (!writer.WriteStringPiece(
+              capsule.datagram_capsule().http_datagram_payload)) {
+        QUIC_BUG(datagram capsule payload write fail)
+            << "Failed to write DATAGRAM CAPSULE payload";
+        return QuicBuffer();
+      }
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      if (!writer.WriteVarInt62(
+              capsule.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 QuicBuffer();
+      }
+      if (!writer.WriteVarInt62(static_cast<uint64_t>(
+              capsule.register_datagram_context_capsule().format_type))) {
+        QUIC_BUG(register context capsule format type write fail)
+            << "Failed to write REGISTER_DATAGRAM_CONTEXT CAPSULE format type";
+        return QuicBuffer();
+      }
+      if (!writer.WriteStringPiece(capsule.register_datagram_context_capsule()
+                                       .format_additional_data)) {
+        QUIC_BUG(register context capsule additional data write fail)
+            << "Failed to write REGISTER_DATAGRAM_CONTEXT CAPSULE additional "
+               "data";
+        return QuicBuffer();
+      }
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      if (!writer.WriteVarInt62(static_cast<uint64_t>(
+              capsule.register_datagram_no_context_capsule().format_type))) {
+        QUIC_BUG(register no context capsule format type write fail)
+            << "Failed to write REGISTER_DATAGRAM_NO_CONTEXT CAPSULE format "
+               "type";
+        return QuicBuffer();
+      }
+      if (!writer.WriteStringPiece(
+              capsule.register_datagram_no_context_capsule()
+                  .format_additional_data)) {
+        QUIC_BUG(register no context capsule additional data write fail)
+            << "Failed to write REGISTER_DATAGRAM_NO_CONTEXT CAPSULE "
+               "additional data";
+        return QuicBuffer();
+      }
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      if (!writer.WriteVarInt62(
+              capsule.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 QuicBuffer();
+      }
+      if (!writer.WriteVarInt62(static_cast<uint64_t>(
+              capsule.close_datagram_context_capsule().close_code))) {
+        QUIC_BUG(close context capsule close code write fail)
+            << "Failed to write CLOSE_DATAGRAM_CONTEXT CAPSULE close code";
+        return QuicBuffer();
+      }
+      if (!writer.WriteStringPiece(
+              capsule.close_datagram_context_capsule().close_details)) {
+        QUIC_BUG(close context capsule close details write fail)
+            << "Failed to write CLOSE_DATAGRAM_CONTEXT CAPSULE close details";
+        return QuicBuffer();
+      }
+      break;
+    default:
+      if (!writer.WriteStringPiece(capsule.unknown_capsule_data())) {
+        QUIC_BUG(capsule data write fail) << "Failed to write CAPSULE data";
+        return QuicBuffer();
+      }
+      break;
+  }
+  if (writer.remaining() != 0) {
+    QUIC_BUG(capsule write length mismatch)
+        << "CAPSULE serialization wrote " << writer.length() << " instead of "
+        << writer.capacity();
+    return QuicBuffer();
+  }
+  return buffer;
+}
+
+bool CapsuleParser::IngestCapsuleFragment(absl::string_view capsule_fragment) {
+  if (parsing_error_occurred_) {
+    return false;
+  }
+  buffered_data_.append(capsule_fragment);
+  while (true) {
+    const size_t buffered_data_read = AttemptParseCapsule();
+    if (parsing_error_occurred_) {
+      QUICHE_DCHECK_EQ(buffered_data_read, 0u);
+      buffered_data_.clear();
+      return false;
+    }
+    if (buffered_data_read == 0) {
+      break;
+    }
+    buffered_data_.erase(0, buffered_data_read);
+  }
+  return true;
+}
+
+size_t CapsuleParser::AttemptParseCapsule() {
+  QUICHE_DCHECK(!parsing_error_occurred_);
+  if (buffered_data_.empty()) {
+    return 0;
+  }
+  QuicDataReader capsule_fragment_reader(buffered_data_);
+  uint64_t capsule_type64;
+  if (!capsule_fragment_reader.ReadVarInt62(&capsule_type64)) {
+    QUIC_DVLOG(2) << "Partial read: not enough data to read capsule type";
+    return 0;
+  }
+  absl::string_view capsule_data;
+  if (!capsule_fragment_reader.ReadStringPieceVarInt62(&capsule_data)) {
+    QUIC_DVLOG(2) << "Partial read: not enough data to read capsule length or "
+                     "full capsule data";
+    return 0;
+  }
+  QuicDataReader capsule_data_reader(capsule_data);
+  Capsule capsule(static_cast<CapsuleType>(capsule_type64));
+  switch (capsule.capsule_type()) {
+    case CapsuleType::DATAGRAM:
+      if (datagram_context_id_present_) {
+        uint64_t context_id;
+        if (!capsule_data_reader.ReadVarInt62(&context_id)) {
+          ReportParseFailure("Unable to parse capsule DATAGRAM context ID");
+          return 0;
+        }
+        capsule.datagram_capsule().context_id = context_id;
+      }
+      capsule.datagram_capsule().http_datagram_payload =
+          capsule_data_reader.ReadRemainingPayload();
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      if (!capsule_data_reader.ReadVarInt62(
+              &capsule.register_datagram_context_capsule().context_id)) {
+        ReportParseFailure(
+            "Unable to parse capsule REGISTER_DATAGRAM_CONTEXT context ID");
+        return 0;
+      }
+      if (!capsule_data_reader.ReadVarInt62(reinterpret_cast<uint64_t*>(
+              &capsule.register_datagram_context_capsule().format_type))) {
+        ReportParseFailure(
+            "Unable to parse capsule REGISTER_DATAGRAM_CONTEXT format type");
+        return 0;
+      }
+      capsule.register_datagram_context_capsule().format_additional_data =
+          capsule_data_reader.ReadRemainingPayload();
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      if (!capsule_data_reader.ReadVarInt62(reinterpret_cast<uint64_t*>(
+              &capsule.register_datagram_no_context_capsule().format_type))) {
+        ReportParseFailure(
+            "Unable to parse capsule REGISTER_DATAGRAM_NO_CONTEXT format type");
+        return 0;
+      }
+      capsule.register_datagram_no_context_capsule().format_additional_data =
+          capsule_data_reader.ReadRemainingPayload();
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      if (!capsule_data_reader.ReadVarInt62(
+              &capsule.close_datagram_context_capsule().context_id)) {
+        ReportParseFailure(
+            "Unable to parse capsule CLOSE_DATAGRAM_CONTEXT context ID");
+        return 0;
+      }
+      if (!capsule_data_reader.ReadVarInt62(reinterpret_cast<uint64_t*>(
+              &capsule.close_datagram_context_capsule().close_code))) {
+        ReportParseFailure(
+            "Unable to parse capsule CLOSE_DATAGRAM_CONTEXT close code");
+        return 0;
+      }
+      capsule.close_datagram_context_capsule().close_details =
+          capsule_data_reader.ReadRemainingPayload();
+      break;
+    default:
+      capsule.unknown_capsule_data() =
+          capsule_data_reader.ReadRemainingPayload();
+  }
+  if (!visitor_->OnCapsule(capsule)) {
+    ReportParseFailure("Visitor failed to process capsule");
+    return 0;
+  }
+  return capsule_fragment_reader.PreviouslyReadPayload().length();
+}
+
+void CapsuleParser::ReportParseFailure(const std::string& error_message) {
+  if (parsing_error_occurred_) {
+    QUIC_BUG(multiple parse errors) << "Experienced multiple parse failures";
+    return;
+  }
+  parsing_error_occurred_ = true;
+  visitor_->OnCapsuleParseFailure(error_message);
+}
+
+void CapsuleParser::ErrorIfThereIsRemainingBufferedData() {
+  if (parsing_error_occurred_) {
+    return;
+  }
+  if (!buffered_data_.empty()) {
+    ReportParseFailure("Incomplete capsule left at the end of the stream");
+  }
+}
+
+}  // namespace quic
diff --git a/quic/core/http/capsule.h b/quic/core/http/capsule.h
new file mode 100644
index 0000000..664633a
--- /dev/null
+++ b/quic/core/http/capsule.h
@@ -0,0 +1,234 @@
+// Copyright (c) 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef QUICHE_QUIC_CORE_HTTP_CAPSULE_H_
+#define QUICHE_QUIC_CORE_HTTP_CAPSULE_H_
+
+#include <cstdint>
+#include <string>
+
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "quic/core/quic_buffer_allocator.h"
+#include "quic/core/quic_data_reader.h"
+#include "quic/core/quic_types.h"
+#include "common/platform/api/quiche_logging.h"
+
+namespace quic {
+
+enum class CapsuleType : uint64_t {
+  // Casing in this enum matches the IETF specification.
+  DATAGRAM = 0xff37a0,
+  REGISTER_DATAGRAM_CONTEXT = 0xff37a1,
+  REGISTER_DATAGRAM_NO_CONTEXT = 0xff37a2,
+  CLOSE_DATAGRAM_CONTEXT = 0xff37a3,
+};
+
+QUIC_EXPORT_PRIVATE std::string CapsuleTypeToString(CapsuleType capsule_type);
+QUIC_EXPORT_PRIVATE std::ostream& operator<<(std::ostream& os,
+                                             const CapsuleType& capsule_type);
+
+enum class DatagramFormatType : uint64_t {
+  // Casing in this enum matches the IETF specification.
+  UDP_PAYLOAD = 0xff6f00,
+  WEBTRANSPORT = 0xff7c00,
+};
+
+QUIC_EXPORT_PRIVATE std::string DatagramFormatTypeToString(
+    DatagramFormatType datagram_format_type);
+QUIC_EXPORT_PRIVATE std::ostream& operator<<(
+    std::ostream& os, const DatagramFormatType& datagram_format_type);
+
+enum class ContextCloseCode : uint64_t {
+  // Casing in this enum matches the IETF specification.
+  NO_ERROR = 0xff78a0,
+  UNKNOWN_FORMAT = 0xff78a1,
+  DENIED = 0xff78a2,
+  RESOURCE_LIMIT = 0xff78a3,
+};
+
+QUIC_EXPORT_PRIVATE std::string ContextCloseCodeToString(
+    ContextCloseCode context_close_code);
+QUIC_EXPORT_PRIVATE std::ostream& operator<<(
+    std::ostream& os, const ContextCloseCode& context_close_code);
+
+struct QUIC_EXPORT_PRIVATE DatagramCapsule {
+  absl::optional<QuicDatagramContextId> context_id;
+  absl::string_view http_datagram_payload;
+};
+struct QUIC_EXPORT_PRIVATE RegisterDatagramContextCapsule {
+  QuicDatagramContextId context_id;
+  DatagramFormatType format_type;
+  absl::string_view format_additional_data;
+};
+struct QUIC_EXPORT_PRIVATE RegisterDatagramNoContextCapsule {
+  DatagramFormatType format_type;
+  absl::string_view format_additional_data;
+};
+struct QUIC_EXPORT_PRIVATE CloseDatagramContextCapsule {
+  QuicDatagramContextId context_id;
+  ContextCloseCode close_code;
+  absl::string_view close_details;
+};
+
+// Capsule from draft-ietf-masque-h3-datagram.
+// IMPORTANT NOTE: Capsule does not own any of the absl::string_view memory it
+// points to. Strings saved into a capsule must outlive the capsule object. Any
+// code that sees a capsule in a callback needs to either process it immediately
+// or perform its own deep copy.
+class QUIC_EXPORT_PRIVATE Capsule {
+ public:
+  static Capsule Datagram(
+      absl::optional<QuicDatagramContextId> context_id = absl::nullopt,
+      absl::string_view http_datagram_payload = absl::string_view());
+  static Capsule RegisterDatagramContext(
+      QuicDatagramContextId context_id, DatagramFormatType format_type,
+      absl::string_view format_additional_data = absl::string_view());
+  static Capsule RegisterDatagramNoContext(
+      DatagramFormatType format_type,
+      absl::string_view format_additional_data = absl::string_view());
+  static Capsule CloseDatagramContext(
+      QuicDatagramContextId context_id,
+      ContextCloseCode close_code = ContextCloseCode::NO_ERROR,
+      absl::string_view close_details = absl::string_view());
+  static Capsule Unknown(
+      uint64_t capsule_type,
+      absl::string_view unknown_capsule_data = absl::string_view());
+
+  explicit Capsule(CapsuleType capsule_type);
+  Capsule(const Capsule& other);
+  Capsule& operator=(const Capsule& other);
+  bool operator==(const Capsule& other) const;
+
+  // Human-readable information string for debugging purposes.
+  std::string ToString() const;
+  friend QUIC_EXPORT_PRIVATE std::ostream& operator<<(std::ostream& os,
+                                                      const Capsule& capsule);
+
+  CapsuleType capsule_type() const { return capsule_type_; }
+  DatagramCapsule& datagram_capsule() {
+    QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::DATAGRAM);
+    return datagram_capsule_;
+  }
+  const DatagramCapsule& datagram_capsule() const {
+    QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::DATAGRAM);
+    return datagram_capsule_;
+  }
+  RegisterDatagramContextCapsule& register_datagram_context_capsule() {
+    QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::REGISTER_DATAGRAM_CONTEXT);
+    return register_datagram_context_capsule_;
+  }
+  const RegisterDatagramContextCapsule& register_datagram_context_capsule()
+      const {
+    QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::REGISTER_DATAGRAM_CONTEXT);
+    return register_datagram_context_capsule_;
+  }
+  RegisterDatagramNoContextCapsule& register_datagram_no_context_capsule() {
+    QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT);
+    return register_datagram_no_context_capsule_;
+  }
+  const RegisterDatagramNoContextCapsule& register_datagram_no_context_capsule()
+      const {
+    QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT);
+    return register_datagram_no_context_capsule_;
+  }
+  CloseDatagramContextCapsule& close_datagram_context_capsule() {
+    QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::CLOSE_DATAGRAM_CONTEXT);
+    return close_datagram_context_capsule_;
+  }
+  const CloseDatagramContextCapsule& close_datagram_context_capsule() const {
+    QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::CLOSE_DATAGRAM_CONTEXT);
+    return close_datagram_context_capsule_;
+  }
+  absl::string_view& unknown_capsule_data() {
+    QUICHE_DCHECK(capsule_type_ != CapsuleType::DATAGRAM &&
+                  capsule_type_ != CapsuleType::REGISTER_DATAGRAM_CONTEXT &&
+                  capsule_type_ != CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT &&
+                  capsule_type_ != CapsuleType::CLOSE_DATAGRAM_CONTEXT)
+        << capsule_type_;
+    return unknown_capsule_data_;
+  }
+  const absl::string_view& unknown_capsule_data() const {
+    QUICHE_DCHECK(capsule_type_ != CapsuleType::DATAGRAM &&
+                  capsule_type_ != CapsuleType::REGISTER_DATAGRAM_CONTEXT &&
+                  capsule_type_ != CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT &&
+                  capsule_type_ != CapsuleType::CLOSE_DATAGRAM_CONTEXT)
+        << capsule_type_;
+    return unknown_capsule_data_;
+  }
+
+ private:
+  CapsuleType capsule_type_;
+  union {
+    DatagramCapsule datagram_capsule_;
+    RegisterDatagramContextCapsule register_datagram_context_capsule_;
+    RegisterDatagramNoContextCapsule register_datagram_no_context_capsule_;
+    CloseDatagramContextCapsule close_datagram_context_capsule_;
+    absl::string_view unknown_capsule_data_;
+  };
+};
+
+namespace test {
+class CapsuleParserPeer;
+}  // namespace test
+
+class QUIC_EXPORT_PRIVATE CapsuleParser {
+ public:
+  class QUIC_EXPORT_PRIVATE Visitor {
+   public:
+    virtual ~Visitor() {}
+
+    // Called when a capsule has been successfully parsed. The return value
+    // indicates whether the contents of the capsule are valid: if false is
+    // returned, the parse operation will be considered failed and
+    // OnCapsuleParseFailure will be called. Note that since Capsule does not
+    // own the memory backing its string_views, that memory is only valid until
+    // this callback returns. Visitors that wish to access the capsule later
+    // MUST make a deep copy before this returns.
+    virtual bool OnCapsule(const Capsule& capsule) = 0;
+
+    virtual void OnCapsuleParseFailure(const std::string& error_message) = 0;
+  };
+
+  // |visitor| must be non-null, and must outlive CapsuleParser.
+  explicit CapsuleParser(Visitor* visitor);
+
+  void set_datagram_context_id_present(bool datagram_context_id_present) {
+    datagram_context_id_present_ = datagram_context_id_present;
+  }
+
+  // Ingests a capsule fragment (any fragment of bytes from the capsule data
+  // stream) and parses and complete capsules it encounters. Returns false if a
+  // parsing error occurred.
+  bool IngestCapsuleFragment(absl::string_view capsule_fragment);
+
+  void ErrorIfThereIsRemainingBufferedData();
+
+  friend class test::CapsuleParserPeer;
+
+ private:
+  // Attempts to parse a single capsule from |buffered_data_|. If a full capsule
+  // is not available, returns 0. If a parsing error occurs, returns 0.
+  // Otherwise, returns the number of bytes in the parsed capsule.
+  size_t AttemptParseCapsule();
+  void ReportParseFailure(const std::string& error_message);
+
+  // Whether HTTP Datagram Context IDs are present.
+  bool datagram_context_id_present_ = false;
+  // Whether a parsing error has occurred.
+  bool parsing_error_occurred_ = false;
+  // Visitor which will receive callbacks, unowned.
+  Visitor* visitor_;
+
+  std::string buffered_data_;
+};
+
+// Serializes |capsule| into a newly allocated buffer.
+QUIC_EXPORT_PRIVATE QuicBuffer SerializeCapsule(const Capsule& capsule,
+                                                QuicBufferAllocator* allocator);
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_CORE_HTTP_CAPSULE_H_
diff --git a/quic/core/http/capsule_test.cc b/quic/core/http/capsule_test.cc
new file mode 100644
index 0000000..634cdae
--- /dev/null
+++ b/quic/core/http/capsule_test.cc
@@ -0,0 +1,299 @@
+// Copyright (c) 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quic/core/http/capsule.h"
+
+#include <cstddef>
+#include <deque>
+#include <string>
+#include <vector>
+
+#include "absl/strings/escaping.h"
+#include "absl/strings/string_view.h"
+#include "quic/platform/api/quic_test.h"
+#include "quic/test_tools/quic_test_utils.h"
+#include "common/test_tools/quiche_test_utils.h"
+
+using ::testing::_;
+using ::testing::InSequence;
+using ::testing::Return;
+
+namespace quic {
+namespace test {
+
+namespace {
+constexpr DatagramFormatType kFakeFormatType =
+    static_cast<DatagramFormatType>(0x123456);
+constexpr ContextCloseCode kFakeCloseCode =
+    static_cast<ContextCloseCode>(0x654321);
+}  // namespace
+
+class CapsuleParserPeer {
+ public:
+  static std::string* buffered_data(CapsuleParser* capsule_parser) {
+    return &capsule_parser->buffered_data_;
+  }
+};
+
+class MockVisitor : public CapsuleParser::Visitor {
+ public:
+  MockVisitor() { ON_CALL(*this, OnCapsule(_)).WillByDefault(Return(true)); }
+  ~MockVisitor() override = default;
+  MOCK_METHOD(bool, OnCapsule, (const Capsule& capsule), (override));
+  MOCK_METHOD(void, OnCapsuleParseFailure, (const std::string& error_message),
+              (override));
+};
+
+class CapsuleTest : public QuicTest {
+ public:
+  CapsuleTest() : capsule_parser_(&visitor_) {}
+
+  void ValidateParserIsEmpty() {
+    EXPECT_CALL(visitor_, OnCapsule(_)).Times(0);
+    EXPECT_CALL(visitor_, OnCapsuleParseFailure(_)).Times(0);
+    capsule_parser_.ErrorIfThereIsRemainingBufferedData();
+    EXPECT_TRUE(CapsuleParserPeer::buffered_data(&capsule_parser_)->empty());
+  }
+
+  void TestSerialization(const Capsule& capsule,
+                         const std::string& expected_bytes) {
+    QuicBuffer serialized_capsule =
+        SerializeCapsule(capsule, SimpleBufferAllocator::Get());
+    quiche::test::CompareCharArraysWithHexError(
+        "Serialized capsule", serialized_capsule.data(),
+        serialized_capsule.size(), expected_bytes.data(),
+        expected_bytes.size());
+  }
+
+  ::testing::StrictMock<MockVisitor> visitor_;
+  CapsuleParser capsule_parser_;
+};
+
+TEST_F(CapsuleTest, DatagramCapsule) {
+  std::string capsule_fragment = absl::HexStringToBytes(
+      "80ff37a0"          // DATAGRAM capsule type
+      "08"                // capsule length
+      "a1a2a3a4a5a6a7a8"  // HTTP Datagram payload
+  );
+  std::string datagram_payload = absl::HexStringToBytes("a1a2a3a4a5a6a7a8");
+  Capsule expected_capsule =
+      Capsule::Datagram(/*context_id=*/absl::nullopt, datagram_payload);
+  {
+    EXPECT_CALL(visitor_, OnCapsule(expected_capsule));
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment));
+  }
+  ValidateParserIsEmpty();
+  TestSerialization(expected_capsule, capsule_fragment);
+}
+
+TEST_F(CapsuleTest, DatagramCapsuleWithContext) {
+  std::string capsule_fragment = absl::HexStringToBytes(
+      "80ff37a0"          // DATAGRAM capsule type
+      "09"                // capsule length
+      "04"                // context ID
+      "a1a2a3a4a5a6a7a8"  // HTTP Datagram payload
+  );
+  capsule_parser_.set_datagram_context_id_present(true);
+  std::string datagram_payload = absl::HexStringToBytes("a1a2a3a4a5a6a7a8");
+  Capsule expected_capsule =
+      Capsule::Datagram(/*context_id=*/4, datagram_payload);
+  {
+    EXPECT_CALL(visitor_, OnCapsule(expected_capsule));
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment));
+  }
+  ValidateParserIsEmpty();
+  TestSerialization(expected_capsule, capsule_fragment);
+}
+
+TEST_F(CapsuleTest, RegisterContextCapsule) {
+  std::string capsule_fragment = absl::HexStringToBytes(
+      "80ff37a1"          // REGISTER_DATAGRAM_CONTEXT capsule type
+      "0d"                // capsule length
+      "04"                // context ID
+      "80123456"          // 0x123456 datagram format type
+      "f1f2f3f4f5f6f7f8"  // format additional data
+  );
+  std::string format_additional_data =
+      absl::HexStringToBytes("f1f2f3f4f5f6f7f8");
+  Capsule expected_capsule = Capsule::RegisterDatagramContext(
+      /*context_id=*/4, kFakeFormatType, format_additional_data);
+  {
+    EXPECT_CALL(visitor_, OnCapsule(expected_capsule));
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment));
+  }
+  ValidateParserIsEmpty();
+  TestSerialization(expected_capsule, capsule_fragment);
+}
+
+TEST_F(CapsuleTest, RegisterNoContextCapsule) {
+  std::string capsule_fragment = absl::HexStringToBytes(
+      "80ff37a2"          // REGISTER_DATAGRAM_NO_CONTEXT capsule type
+      "0c"                // capsule length
+      "80123456"          // 0x123456 datagram format type
+      "f1f2f3f4f5f6f7f8"  // format additional data
+  );
+  std::string format_additional_data =
+      absl::HexStringToBytes("f1f2f3f4f5f6f7f8");
+  Capsule expected_capsule = Capsule::RegisterDatagramNoContext(
+      kFakeFormatType, format_additional_data);
+  {
+    EXPECT_CALL(visitor_, OnCapsule(expected_capsule));
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment));
+  }
+  ValidateParserIsEmpty();
+  TestSerialization(expected_capsule, capsule_fragment);
+}
+
+TEST_F(CapsuleTest, CloseContextCapsule) {
+  std::string capsule_fragment = absl::HexStringToBytes(
+      "80ff37a3"  // CLOSE_DATAGRAM_CONTEXT capsule type
+      "27"        // capsule length
+      "04"        // context ID
+      "80654321"  // 0x654321 close code
+  );
+  std::string close_details = "All your contexts are belong to us";
+  capsule_fragment += close_details;
+  Capsule expected_capsule = Capsule::CloseDatagramContext(
+      /*context_id=*/4, kFakeCloseCode, close_details);
+  {
+    EXPECT_CALL(visitor_, OnCapsule(expected_capsule));
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment));
+  }
+  ValidateParserIsEmpty();
+  TestSerialization(expected_capsule, capsule_fragment);
+}
+
+TEST_F(CapsuleTest, UnknownCapsule) {
+  std::string capsule_fragment = absl::HexStringToBytes(
+      "33"                // unknown capsule type of 0x33
+      "08"                // capsule length
+      "a1a2a3a4a5a6a7a8"  // unknown capsule data
+  );
+  std::string unknown_capsule_data = absl::HexStringToBytes("a1a2a3a4a5a6a7a8");
+  Capsule expected_capsule = Capsule::Unknown(0x33, unknown_capsule_data);
+  {
+    EXPECT_CALL(visitor_, OnCapsule(expected_capsule));
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment));
+  }
+  ValidateParserIsEmpty();
+  TestSerialization(expected_capsule, capsule_fragment);
+}
+
+TEST_F(CapsuleTest, TwoDatagramCapsules) {
+  std::string capsule_fragment = absl::HexStringToBytes(
+      "80ff37a0"          // DATAGRAM capsule type
+      "08"                // capsule length
+      "a1a2a3a4a5a6a7a8"  // HTTP Datagram payload
+      "80ff37a0"          // DATAGRAM capsule type
+      "08"                // capsule length
+      "b1b2b3b4b5b6b7b8"  // HTTP Datagram payload
+  );
+  std::string datagram_payload1 = absl::HexStringToBytes("a1a2a3a4a5a6a7a8");
+  std::string datagram_payload2 = absl::HexStringToBytes("b1b2b3b4b5b6b7b8");
+  Capsule expected_capsule1 =
+      Capsule::Datagram(/*context_id=*/absl::nullopt, datagram_payload1);
+  Capsule expected_capsule2 =
+      Capsule::Datagram(/*context_id=*/absl::nullopt, datagram_payload2);
+  {
+    InSequence s;
+    EXPECT_CALL(visitor_, OnCapsule(expected_capsule1));
+    EXPECT_CALL(visitor_, OnCapsule(expected_capsule2));
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment));
+  }
+  ValidateParserIsEmpty();
+}
+
+TEST_F(CapsuleTest, TwoDatagramCapsulesPartialReads) {
+  std::string capsule_fragment1 = absl::HexStringToBytes(
+      "80ff37a0"  // first capsule DATAGRAM capsule type
+      "08"        // frist capsule length
+      "a1a2a3a4"  // first half of HTTP Datagram payload of first capsule
+  );
+  std::string capsule_fragment2 = absl::HexStringToBytes(
+      "a5a6a7a8"  // second half of HTTP Datagram payload 1
+      "80ff37a0"  // second capsule DATAGRAM capsule type
+  );
+  std::string capsule_fragment3 = absl::HexStringToBytes(
+      "08"                // second capsule length
+      "b1b2b3b4b5b6b7b8"  // HTTP Datagram payload of second capsule
+  );
+  capsule_parser_.ErrorIfThereIsRemainingBufferedData();
+  std::string datagram_payload1 = absl::HexStringToBytes("a1a2a3a4a5a6a7a8");
+  std::string datagram_payload2 = absl::HexStringToBytes("b1b2b3b4b5b6b7b8");
+  Capsule expected_capsule1 =
+      Capsule::Datagram(/*context_id=*/absl::nullopt, datagram_payload1);
+  Capsule expected_capsule2 =
+      Capsule::Datagram(/*context_id=*/absl::nullopt, datagram_payload2);
+  {
+    InSequence s;
+    EXPECT_CALL(visitor_, OnCapsule(expected_capsule1));
+    EXPECT_CALL(visitor_, OnCapsule(expected_capsule2));
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment1));
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment2));
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment3));
+  }
+  ValidateParserIsEmpty();
+}
+
+TEST_F(CapsuleTest, TwoDatagramCapsulesOneByteAtATime) {
+  std::string capsule_fragment = absl::HexStringToBytes(
+      "80ff37a0"          // DATAGRAM capsule type
+      "08"                // capsule length
+      "a1a2a3a4a5a6a7a8"  // HTTP Datagram payload
+      "80ff37a0"          // DATAGRAM capsule type
+      "08"                // capsule length
+      "b1b2b3b4b5b6b7b8"  // HTTP Datagram payload
+  );
+  std::string datagram_payload1 = absl::HexStringToBytes("a1a2a3a4a5a6a7a8");
+  std::string datagram_payload2 = absl::HexStringToBytes("b1b2b3b4b5b6b7b8");
+  Capsule expected_capsule1 =
+      Capsule::Datagram(/*context_id=*/absl::nullopt, datagram_payload1);
+  Capsule expected_capsule2 =
+      Capsule::Datagram(/*context_id=*/absl::nullopt, datagram_payload2);
+  for (size_t i = 0; i < capsule_fragment.size(); i++) {
+    if (i < capsule_fragment.size() / 2 - 1) {
+      EXPECT_CALL(visitor_, OnCapsule(_)).Times(0);
+      ASSERT_TRUE(
+          capsule_parser_.IngestCapsuleFragment(capsule_fragment.substr(i, 1)));
+    } else if (i == capsule_fragment.size() / 2 - 1) {
+      EXPECT_CALL(visitor_, OnCapsule(expected_capsule1));
+      ASSERT_TRUE(
+          capsule_parser_.IngestCapsuleFragment(capsule_fragment.substr(i, 1)));
+      EXPECT_TRUE(CapsuleParserPeer::buffered_data(&capsule_parser_)->empty());
+    } else if (i < capsule_fragment.size() - 1) {
+      EXPECT_CALL(visitor_, OnCapsule(_)).Times(0);
+      ASSERT_TRUE(
+          capsule_parser_.IngestCapsuleFragment(capsule_fragment.substr(i, 1)));
+    } else {
+      EXPECT_CALL(visitor_, OnCapsule(expected_capsule2));
+      ASSERT_TRUE(
+          capsule_parser_.IngestCapsuleFragment(capsule_fragment.substr(i, 1)));
+      EXPECT_TRUE(CapsuleParserPeer::buffered_data(&capsule_parser_)->empty());
+    }
+  }
+  capsule_parser_.ErrorIfThereIsRemainingBufferedData();
+  EXPECT_TRUE(CapsuleParserPeer::buffered_data(&capsule_parser_)->empty());
+}
+
+TEST_F(CapsuleTest, PartialCapsuleThenError) {
+  std::string capsule_fragment = absl::HexStringToBytes(
+      "80ff37a0"  // DATAGRAM capsule type
+      "08"        // capsule length
+      "a1a2a3a4"  // first half of HTTP Datagram payload
+  );
+  EXPECT_CALL(visitor_, OnCapsule(_)).Times(0);
+  {
+    EXPECT_CALL(visitor_, OnCapsuleParseFailure(_)).Times(0);
+    ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment));
+  }
+  {
+    EXPECT_CALL(visitor_,
+                OnCapsuleParseFailure(
+                    "Incomplete capsule left at the end of the stream"));
+    capsule_parser_.ErrorIfThereIsRemainingBufferedData();
+  }
+}
+
+}  // namespace test
+}  // namespace quic
diff --git a/quic/core/http/http_constants.cc b/quic/core/http/http_constants.cc
index f3ed523..851fd91 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_DRAFT04);
     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..ce03a1e 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-04.
+  SETTINGS_H3_DATAGRAM_DRAFT04 = 0xffd277,
   // draft-ietf-webtrans-http3-00
   SETTINGS_WEBTRANS_DRAFT00 = 0x2b603742,
 };
diff --git a/quic/core/http/quic_send_control_stream_test.cc b/quic/core/http/quic_send_control_stream_test.cc
index 45de224..5943585 100644
--- a/quic/core/http/quic_send_control_stream_test.cc
+++ b/quic/core/http/quic_send_control_stream_test.cc
@@ -71,9 +71,7 @@
  public:
   QuicSendControlStreamTest()
       : connection_(new StrictMock<MockQuicConnection>(
-            &helper_,
-            &alarm_factory_,
-            perspective(),
+            &helper_, &alarm_factory_, perspective(),
             SupportedVersions(GetParam().version))),
         session_(connection_) {
     ON_CALL(session_, WritevData(_, _, _, _, _, _))
@@ -104,8 +102,7 @@
   QuicSendControlStream* send_control_stream_;
 };
 
-INSTANTIATE_TEST_SUITE_P(Tests,
-                         QuicSendControlStreamTest,
+INSTANTIATE_TEST_SUITE_P(Tests, QuicSendControlStreamTest,
                          ::testing::ValuesIn(GetTestParams()),
                          ::testing::PrintToStringParamName());
 
@@ -133,24 +130,27 @@
       "4040"  // 0x40 as the reserved frame type
       "01"    // 1 byte frame length
       "61");  //  payload "a"
-  if (QuicSpdySessionPeer::ShouldNegotiateHttp3Datagram(&session_)) {
+  if (QuicSpdySessionPeer::LocalHttpDatagramSupport(&session_) ==
+      HttpDatagramSupport::kDraft00And04) {
     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
+        "800ffd277"  // SETTINGS_H3_DATAGRAM_DRAFT04
+        "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 06531a5..0cd2644 100644
--- a/quic/core/http/quic_spdy_session.cc
+++ b/quic/core/http/quic_spdy_session.cc
@@ -193,8 +193,7 @@
     header_list_.Clear();
   }
 
-  void OnStreamFrameData(SpdyStreamId /*stream_id*/,
-                         const char* /*data*/,
+  void OnStreamFrameData(SpdyStreamId /*stream_id*/, const char* /*data*/,
                          size_t /*len*/) override {
     QUICHE_DCHECK(!VersionUsesHttp3(session_->transport_version()));
     CloseConnection("SPDY DATA frame received.",
@@ -282,8 +281,7 @@
         code);
   }
 
-  void OnDataFrameHeader(SpdyStreamId /*stream_id*/,
-                         size_t /*length*/,
+  void OnDataFrameHeader(SpdyStreamId /*stream_id*/, size_t /*length*/,
                          bool /*fin*/) override {
     QUICHE_DCHECK(!VersionUsesHttp3(session_->transport_version()));
     CloseConnection("SPDY DATA frame received.",
@@ -316,13 +314,9 @@
                     QUIC_INVALID_HEADERS_STREAM_DATA);
   }
 
-  void OnHeaders(SpdyStreamId stream_id,
-                 bool has_priority,
-                 int weight,
-                 SpdyStreamId /* parent_stream_id */,
-                 bool /* exclusive */,
-                 bool fin,
-                 bool /*end*/) override {
+  void OnHeaders(SpdyStreamId stream_id, bool has_priority, int weight,
+                 SpdyStreamId /* parent_stream_id */, bool /* exclusive */,
+                 bool fin, bool /*end*/) override {
     if (!session_->IsConnected()) {
       return;
     }
@@ -350,8 +344,7 @@
                     QUIC_INVALID_HEADERS_STREAM_DATA);
   }
 
-  void OnPushPromise(SpdyStreamId stream_id,
-                     SpdyStreamId promised_stream_id,
+  void OnPushPromise(SpdyStreamId stream_id, SpdyStreamId promised_stream_id,
                      bool /*end*/) override {
     QUICHE_DCHECK(!VersionUsesHttp3(session_->transport_version()));
     if (session_->perspective() != Perspective::IS_CLIENT) {
@@ -367,10 +360,8 @@
 
   void OnContinuation(SpdyStreamId /*stream_id*/, bool /*end*/) override {}
 
-  void OnPriority(SpdyStreamId stream_id,
-                  SpdyStreamId /* parent_id */,
-                  int weight,
-                  bool /* exclusive */) override {
+  void OnPriority(SpdyStreamId stream_id, SpdyStreamId /* parent_id */,
+                  int weight, bool /* exclusive */) override {
     QUICHE_DCHECK(!VersionUsesHttp3(session_->transport_version()));
     if (!session_->IsConnected()) {
       return;
@@ -393,10 +384,8 @@
   }
 
   // SpdyFramerDebugVisitorInterface implementation
-  void OnSendCompressedFrame(SpdyStreamId /*stream_id*/,
-                             SpdyFrameType /*type*/,
-                             size_t payload_len,
-                             size_t frame_len) override {
+  void OnSendCompressedFrame(SpdyStreamId /*stream_id*/, SpdyFrameType /*type*/,
+                             size_t payload_len, size_t frame_len) override {
     if (payload_len == 0) {
       QUIC_BUG(quic_bug_10360_1) << "Zero payload length.";
       return;
@@ -518,8 +507,17 @@
       qpack_maximum_blocked_streams_;
   settings_.values[SETTINGS_MAX_FIELD_SECTION_SIZE] =
       max_inbound_header_list_size_;
-  if (ShouldNegotiateHttp3Datagram() && version().UsesHttp3()) {
-    settings_.values[SETTINGS_H3_DATAGRAM] = 1;
+  if (version().UsesHttp3()) {
+    HttpDatagramSupport local_http_datagram_support =
+        LocalHttpDatagramSupport();
+    if (local_http_datagram_support == HttpDatagramSupport::kDraft00 ||
+        local_http_datagram_support == HttpDatagramSupport::kDraft00And04) {
+      settings_.values[SETTINGS_H3_DATAGRAM_DRAFT00] = 1;
+    }
+    if (local_http_datagram_support == HttpDatagramSupport::kDraft04 ||
+        local_http_datagram_support == HttpDatagramSupport::kDraft00And04) {
+      settings_.values[SETTINGS_H3_DATAGRAM_DRAFT04] = 1;
+    }
   }
   if (WillNegotiateWebTransport()) {
     settings_.values[SETTINGS_WEBTRANS_DRAFT00] = 1;
@@ -543,8 +541,7 @@
 }
 
 void QuicSpdySession::OnStreamHeadersPriority(
-    QuicStreamId stream_id,
-    const spdy::SpdyStreamPrecedence& precedence) {
+    QuicStreamId stream_id, const spdy::SpdyStreamPrecedence& precedence) {
   QuicSpdyStream* stream = GetOrCreateSpdyDataStream(stream_id);
   if (!stream) {
     // It's quite possible to receive headers after a stream has been reset.
@@ -553,8 +550,7 @@
   stream->OnStreamHeadersPriority(precedence);
 }
 
-void QuicSpdySession::OnStreamHeaderList(QuicStreamId stream_id,
-                                         bool fin,
+void QuicSpdySession::OnStreamHeaderList(QuicStreamId stream_id, bool fin,
                                          size_t frame_len,
                                          const QuicHeaderList& header_list) {
   if (IsStaticStream(stream_id)) {
@@ -593,8 +589,7 @@
 }
 
 void QuicSpdySession::OnPriorityFrame(
-    QuicStreamId stream_id,
-    const spdy::SpdyStreamPrecedence& precedence) {
+    QuicStreamId stream_id, const spdy::SpdyStreamPrecedence& precedence) {
   QuicSpdyStream* stream = GetOrCreateSpdyDataStream(stream_id);
   if (!stream) {
     // It's quite possible to receive a PRIORITY frame after a stream has been
@@ -671,9 +666,7 @@
 }
 
 size_t QuicSpdySession::WriteHeadersOnHeadersStream(
-    QuicStreamId id,
-    SpdyHeaderBlock headers,
-    bool fin,
+    QuicStreamId id, SpdyHeaderBlock headers, bool fin,
     const spdy::SpdyStreamPrecedence& precedence,
     QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener) {
   QUICHE_DCHECK(!VersionUsesHttp3(transport_version()));
@@ -686,8 +679,7 @@
 }
 
 size_t QuicSpdySession::WritePriority(QuicStreamId id,
-                                      QuicStreamId parent_stream_id,
-                                      int weight,
+                                      QuicStreamId parent_stream_id, int weight,
                                       bool exclusive) {
   QUICHE_DCHECK(!VersionUsesHttp3(transport_version()));
   SpdyPriorityIR priority_frame(id, parent_stream_id, weight, exclusive);
@@ -865,8 +857,7 @@
 }
 
 void QuicSpdySession::OnNewEncryptionKeyAvailable(
-    EncryptionLevel level,
-    std::unique_ptr<QuicEncrypter> encrypter) {
+    EncryptionLevel level, std::unique_ptr<QuicEncrypter> encrypter) {
   QuicSession::OnNewEncryptionKeyAvailable(level, std::move(encrypter));
   if (IsEncryptionEstablished()) {
     // Send H3 SETTINGs once encryption is established.
@@ -874,13 +865,11 @@
   }
 }
 
-bool QuicSpdySession::ShouldNegotiateWebTransport() {
-  return false;
-}
+bool QuicSpdySession::ShouldNegotiateWebTransport() { return false; }
 
 bool QuicSpdySession::WillNegotiateWebTransport() {
-  return ShouldNegotiateHttp3Datagram() && version().UsesHttp3() &&
-         ShouldNegotiateWebTransport();
+  return LocalHttpDatagramSupport() != HttpDatagramSupport::kNone &&
+         version().UsesHttp3() && ShouldNegotiateWebTransport();
 }
 
 // True if there are open HTTP requests.
@@ -902,12 +891,8 @@
 }
 
 size_t QuicSpdySession::WriteHeadersOnHeadersStreamImpl(
-    QuicStreamId id,
-    spdy::SpdyHeaderBlock headers,
-    bool fin,
-    QuicStreamId parent_stream_id,
-    int weight,
-    bool exclusive,
+    QuicStreamId id, spdy::SpdyHeaderBlock headers, bool fin,
+    QuicStreamId parent_stream_id, int weight, bool exclusive,
     QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener) {
   QUICHE_DCHECK(!VersionUsesHttp3(transport_version()));
 
@@ -941,10 +926,8 @@
 }
 
 void QuicSpdySession::OnPromiseHeaderList(
-    QuicStreamId /*stream_id*/,
-    QuicStreamId /*promised_stream_id*/,
-    size_t /*frame_len*/,
-    const QuicHeaderList& /*header_list*/) {
+    QuicStreamId /*stream_id*/, QuicStreamId /*promised_stream_id*/,
+    size_t /*frame_len*/, const QuicHeaderList& /*header_list*/) {
   std::string error =
       "OnPromiseHeaderList should be overridden in client code.";
   QUIC_BUG(quic_bug_10360_6) << error;
@@ -974,8 +957,7 @@
 }
 
 absl::optional<std::string> QuicSpdySession::OnAlpsData(
-    const uint8_t* alps_data,
-    size_t alps_length) {
+    const uint8_t* alps_data, size_t alps_length) {
   AlpsFrameDecoder alps_frame_decoder(this);
   HttpDecoder decoder(&alps_frame_decoder);
   decoder.ProcessInput(reinterpret_cast<const char*>(alps_data), alps_length);
@@ -1043,6 +1025,21 @@
   return absl::nullopt;
 }
 
+bool QuicSpdySession::VerifySettingIsZeroOrOne(uint64_t id, uint64_t value) {
+  if (value == 0 || value == 1) {
+    return true;
+  }
+  std::string error_details = absl::StrCat(
+      "Received ",
+      H3SettingsToString(static_cast<Http3AndQpackSettingsIdentifiers>(id)),
+      " with invalid value ", value);
+  QUIC_PEER_BUG(bad received setting) << ENDPOINT << error_details;
+  // TODO(dschinazi) use QUIC_HTTP_INVALID_SETTING_VALUE instead of
+  // QUIC_HTTP_RECEIVE_SPDY_SETTING once cl/396439351 lands.
+  CloseConnectionWithDetails(QUIC_HTTP_RECEIVE_SPDY_SETTING, error_details);
+  return false;
+}
+
 bool QuicSpdySession::OnSetting(uint64_t id, uint64_t value) {
   any_settings_received_ = true;
 
@@ -1127,24 +1124,47 @@
             absl::StrCat("received HTTP/2 specific setting in HTTP/3 session: ",
                          id));
         return false;
-      case SETTINGS_H3_DATAGRAM: {
-        if (!ShouldNegotiateHttp3Datagram()) {
+      case SETTINGS_H3_DATAGRAM_DRAFT00: {
+        HttpDatagramSupport local_http_datagram_support =
+            LocalHttpDatagramSupport();
+        if (local_http_datagram_support != HttpDatagramSupport::kDraft00 &&
+            local_http_datagram_support != HttpDatagramSupport::kDraft00And04) {
           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;
-          CloseConnectionWithDetails(QUIC_HTTP_RECEIVE_SPDY_SETTING,
-                                     error_details);
+        if (!VerifySettingIsZeroOrOne(id, value)) {
           return false;
         }
-        h3_datagram_supported_ = !!value;
+        if (value && http_datagram_support_ != HttpDatagramSupport::kDraft04) {
+          // If both draft-00 and draft-04 are supported, use draft-04.
+          http_datagram_support_ = HttpDatagramSupport::kDraft00;
+        }
+        break;
+      }
+      case SETTINGS_H3_DATAGRAM_DRAFT04: {
+        HttpDatagramSupport local_http_datagram_support =
+            LocalHttpDatagramSupport();
+        if (local_http_datagram_support != HttpDatagramSupport::kDraft04 &&
+            local_http_datagram_support != HttpDatagramSupport::kDraft00And04) {
+          break;
+        }
+        QUIC_DVLOG(1) << ENDPOINT
+                      << "SETTINGS_H3_DATAGRAM_DRAFT04 received with value "
+                      << value;
+        if (!version().UsesHttp3()) {
+          break;
+        }
+        if (!VerifySettingIsZeroOrOne(id, value)) {
+          return false;
+        }
+        if (value) {
+          http_datagram_support_ = HttpDatagramSupport::kDraft04;
+        }
         break;
       }
       case SETTINGS_WEBTRANS_DRAFT00:
@@ -1154,14 +1174,7 @@
         QUIC_DVLOG(1) << ENDPOINT
                       << "SETTINGS_ENABLE_WEBTRANSPORT received with value "
                       << value;
-        if (value != 0 && value != 1) {
-          std::string error_details = absl::StrCat(
-              "received SETTINGS_ENABLE_WEBTRANSPORT with invalid value ",
-              value);
-          QUIC_PEER_BUG(invalid SETTINGS_ENABLE_WEBTRANSPORT value)
-              << ENDPOINT << error_details;
-          CloseConnectionWithDetails(QUIC_HTTP_RECEIVE_SPDY_SETTING,
-                                     error_details);
+        if (!VerifySettingIsZeroOrOne(id, value)) {
           return false;
         }
         peer_supports_webtransport_ = (value == 1);
@@ -1234,8 +1247,7 @@
   return false;
 }
 
-void QuicSpdySession::OnHeaders(SpdyStreamId stream_id,
-                                bool has_priority,
+void QuicSpdySession::OnHeaders(SpdyStreamId stream_id, bool has_priority,
                                 const spdy::SpdyStreamPrecedence& precedence,
                                 bool fin) {
   if (has_priority) {
@@ -1572,9 +1584,7 @@
 
 // static
 void QuicSpdySession::LogHeaderCompressionRatioHistogram(
-    bool using_qpack,
-    bool is_sent,
-    QuicByteCount compressed,
+    bool using_qpack, bool is_sent, QuicByteCount compressed,
     QuicByteCount uncompressed) {
   if (compressed <= 0 || uncompressed <= 0) {
     return;
@@ -1620,15 +1630,25 @@
     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 is sent divided by four as per the specification.
+    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;
@@ -1669,8 +1689,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);
@@ -1679,7 +1699,12 @@
     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 ID is sent divided by four as per the specification.
+    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 "
@@ -1709,10 +1734,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()) {
@@ -1743,8 +1772,7 @@
 }
 
 void QuicSpdySession::AssociateIncomingWebTransportStreamWithSession(
-    WebTransportSessionId session_id,
-    QuicStreamId stream_id) {
+    WebTransportSessionId session_id, QuicStreamId stream_id) {
   if (QuicUtils::IsOutgoingStreamId(version(), stream_id, perspective())) {
     QUIC_BUG(AssociateIncomingWebTransportStreamWithSession got outgoing stream)
         << ENDPOINT
@@ -1840,8 +1868,29 @@
   session_->OnDatagramProcessed(status);
 }
 
-bool QuicSpdySession::ShouldNegotiateHttp3Datagram() {
-  return false;
+HttpDatagramSupport QuicSpdySession::LocalHttpDatagramSupport() {
+  return HttpDatagramSupport::kNone;
+}
+
+std::string HttpDatagramSupportToString(
+    HttpDatagramSupport http_datagram_support) {
+  switch (http_datagram_support) {
+    case HttpDatagramSupport::kNone:
+      return "None";
+    case HttpDatagramSupport::kDraft00:
+      return "Draft00";
+    case HttpDatagramSupport::kDraft04:
+      return "Draft04";
+    case HttpDatagramSupport::kDraft00And04:
+      return "Draft00And04";
+  }
+  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
diff --git a/quic/core/http/quic_spdy_session.h b/quic/core/http/quic_spdy_session.h
index bb22f17..d8503cc 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>
@@ -90,8 +91,8 @@
   virtual void OnDataFrameReceived(QuicStreamId /*stream_id*/,
                                    QuicByteCount /*payload_length*/) {}
   virtual void OnHeadersFrameReceived(
-      QuicStreamId /*stream_id*/,
-      QuicByteCount /*compressed_headers_length*/) {}
+      QuicStreamId /*stream_id*/, QuicByteCount /*compressed_headers_length*/) {
+  }
   virtual void OnHeadersDecoded(QuicStreamId /*stream_id*/,
                                 QuicHeaderList /*headers*/) {}
 
@@ -119,6 +120,20 @@
   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,
+  kDraft04 = 2,
+  kDraft00And04 = 3,  // only used locally, we only negotiate one draft.
+};
+
+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,
@@ -126,8 +141,7 @@
       public QpackDecoder::EncoderStreamErrorDelegate {
  public:
   // Does not take ownership of |connection| or |visitor|.
-  QuicSpdySession(QuicConnection* connection,
-                  QuicSession::Visitor* visitor,
+  QuicSpdySession(QuicConnection* connection, QuicSession::Visitor* visitor,
                   const QuicConfig& config,
                   const ParsedQuicVersionVector& supported_versions);
   QuicSpdySession(const QuicSpdySession&) = delete;
@@ -148,14 +162,12 @@
   // Called by |headers_stream_| when headers with a priority have been
   // received for a stream.  This method will only be called for server streams.
   virtual void OnStreamHeadersPriority(
-      QuicStreamId stream_id,
-      const spdy::SpdyStreamPrecedence& precedence);
+      QuicStreamId stream_id, const spdy::SpdyStreamPrecedence& precedence);
 
   // Called by |headers_stream_| when headers have been completely received
   // for a stream.  |fin| will be true if the fin flag was set in the headers
   // frame.
-  virtual void OnStreamHeaderList(QuicStreamId stream_id,
-                                  bool fin,
+  virtual void OnStreamHeaderList(QuicStreamId stream_id, bool fin,
                                   size_t frame_len,
                                   const QuicHeaderList& header_list);
 
@@ -192,18 +204,14 @@
   // If provided, |ack_notifier_delegate| will be registered to be notified when
   // we have seen ACKs for all packets resulting from this call.
   virtual size_t WriteHeadersOnHeadersStream(
-      QuicStreamId id,
-      spdy::SpdyHeaderBlock headers,
-      bool fin,
+      QuicStreamId id, spdy::SpdyHeaderBlock headers, bool fin,
       const spdy::SpdyStreamPrecedence& precedence,
       QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener);
 
   // Writes an HTTP/2 PRIORITY frame the to peer. Returns the size in bytes of
   // the resulting PRIORITY frame.
-  size_t WritePriority(QuicStreamId id,
-                       QuicStreamId parent_stream_id,
-                       int weight,
-                       bool exclusive);
+  size_t WritePriority(QuicStreamId id, QuicStreamId parent_stream_id,
+                       int weight, bool exclusive);
 
   // Writes an HTTP/3 PRIORITY_UPDATE frame to the peer.
   void WriteHttp3PriorityUpdate(const PriorityUpdateFrame& priority_update);
@@ -349,8 +357,7 @@
   // In order for measurements for different protocol to be comparable, the
   // caller must ensure that uncompressed size is the total length of header
   // names and values without any overhead.
-  static void LogHeaderCompressionRatioHistogram(bool using_qpack,
-                                                 bool is_sent,
+  static void LogHeaderCompressionRatioHistogram(bool using_qpack, bool is_sent,
                                                  QuicByteCount compressed,
                                                  QuicByteCount uncompressed);
 
@@ -374,9 +381,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 +409,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.
@@ -414,7 +423,7 @@
   // until the SETTINGS are received.  Only works for HTTP/3.
   bool ShouldBufferRequestsUntilSettings() {
     return version().UsesHttp3() && perspective() == Perspective::IS_SERVER &&
-           ShouldNegotiateHttp3Datagram();
+           LocalHttpDatagramSupport() != HttpDatagramSupport::kNone;
   }
 
   // Returns if the incoming bidirectional streams should process data.  This is
@@ -427,8 +436,7 @@
   // Links the specified stream with a WebTransport session.  If the session is
   // not present, it is buffered until a corresponding stream is found.
   void AssociateIncomingWebTransportStreamWithSession(
-      WebTransportSessionId session_id,
-      QuicStreamId stream_id);
+      WebTransportSessionId session_id, QuicStreamId stream_id);
 
   void ProcessBufferedWebTransportStreamsForSession(WebTransportHttp3* session);
 
@@ -487,17 +495,12 @@
   QuicStream* ProcessPendingStream(PendingStream* pending) override;
 
   size_t WriteHeadersOnHeadersStreamImpl(
-      QuicStreamId id,
-      spdy::SpdyHeaderBlock headers,
-      bool fin,
-      QuicStreamId parent_stream_id,
-      int weight,
-      bool exclusive,
+      QuicStreamId id, spdy::SpdyHeaderBlock headers, bool fin,
+      QuicStreamId parent_stream_id, int weight, bool exclusive,
       QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener);
 
   void OnNewEncryptionKeyAvailable(
-      EncryptionLevel level,
-      std::unique_ptr<QuicEncrypter> encrypter) override;
+      EncryptionLevel level, std::unique_ptr<QuicEncrypter> encrypter) override;
 
   // Sets the maximum size of the header compression table spdy_framer_ is
   // willing to use to encode header blocks.
@@ -520,8 +523,9 @@
   // Called whenever a datagram is dequeued or dropped from datagram_queue().
   virtual void OnDatagramProcessed(absl::optional<MessageStatus> status);
 
-  // Returns true if HTTP/3 datagram extension should be supported.
-  virtual bool ShouldNegotiateHttp3Datagram();
+  // Returns which version of the HTTP/3 datagram extension we should advertise
+  // in settings and accept remote settings for.
+  virtual HttpDatagramSupport LocalHttpDatagramSupport();
 
  private:
   friend class test::QuicSpdySessionPeer;
@@ -547,10 +551,8 @@
   // The following methods are called by the SimpleVisitor.
 
   // Called when a HEADERS frame has been received.
-  void OnHeaders(spdy::SpdyStreamId stream_id,
-                 bool has_priority,
-                 const spdy::SpdyStreamPrecedence& precedence,
-                 bool fin);
+  void OnHeaders(spdy::SpdyStreamId stream_id, bool has_priority,
+                 const spdy::SpdyStreamPrecedence& precedence, bool fin);
 
   // Called when a PRIORITY frame has been received.
   void OnPriority(spdy::SpdyStreamId stream_id,
@@ -568,6 +570,8 @@
 
   void FillSettingsFrame();
 
+  bool VerifySettingIsZeroOrOne(uint64_t id, uint64_t value);
+
   std::unique_ptr<QpackEncoder> qpack_encoder_;
   std::unique_ptr<QpackDecoder> qpack_decoder_;
 
@@ -655,8 +659,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 80296d6..89beb70 100644
--- a/quic/core/http/quic_spdy_session_test.cc
+++ b/quic/core/http/quic_spdy_session_test.cc
@@ -216,8 +216,7 @@
   void OnBodyAvailable() override {}
 
   MOCK_METHOD(void, OnCanWrite, (), (override));
-  MOCK_METHOD(bool,
-              RetransmitStreamData,
+  MOCK_METHOD(bool, RetransmitStreamData,
               (QuicStreamOffset, QuicByteCount, bool, TransmissionType),
               (override));
 
@@ -227,9 +226,7 @@
 class TestSession : public QuicSpdySession {
  public:
   explicit TestSession(QuicConnection* connection)
-      : QuicSpdySession(connection,
-                        nullptr,
-                        DefaultQuicConfig(),
+      : QuicSpdySession(connection, nullptr, DefaultQuicConfig(),
                         CurrentSupportedVersions()),
         crypto_stream_(this),
         writev_consumes_all_data_(false) {
@@ -349,11 +346,11 @@
   bool ShouldNegotiateWebTransport() override { return supports_webtransport_; }
   void set_supports_webtransport(bool value) { supports_webtransport_ = value; }
 
-  bool ShouldNegotiateHttp3Datagram() override {
-    return should_negotiate_h3_datagram_;
+  HttpDatagramSupport LocalHttpDatagramSupport() override {
+    return local_http_datagram_support_;
   }
-  void set_should_negotiate_h3_datagram(bool value) {
-    should_negotiate_h3_datagram_ = value;
+  void set_local_http_datagram_support(HttpDatagramSupport value) {
+    local_http_datagram_support_ = value;
   }
 
   MOCK_METHOD(void, OnAcceptChFrame, (const AcceptChFrame&), (override));
@@ -368,7 +365,7 @@
 
   bool writev_consumes_all_data_;
   bool supports_webtransport_ = false;
-  bool should_negotiate_h3_datagram_ = false;
+  HttpDatagramSupport local_http_datagram_support_ = HttpDatagramSupport::kNone;
 };
 
 class QuicSpdySessionTestBase : public QuicTestWithParam<ParsedQuicVersion> {
@@ -383,11 +380,9 @@
 
  protected:
   explicit QuicSpdySessionTestBase(Perspective perspective)
-      : connection_(
-            new StrictMock<MockQuicConnection>(&helper_,
-                                               &alarm_factory_,
-                                               perspective,
-                                               SupportedVersions(GetParam()))),
+      : connection_(new StrictMock<MockQuicConnection>(
+            &helper_, &alarm_factory_, perspective,
+            SupportedVersions(GetParam()))),
         session_(connection_) {
     session_.config()->SetInitialStreamFlowControlWindowToSend(
         kInitialStreamFlowControlWindowForTest);
@@ -506,8 +501,7 @@
   }
 
   QuicStreamId StreamCountToId(QuicStreamCount stream_count,
-                               Perspective perspective,
-                               bool bidirectional) {
+                               Perspective perspective, bool bidirectional) {
     // Calculate and build up stream ID rather than use
     // GetFirst... because the test that relies on this method
     // needs to do the stream count where #1 is 0/1/2/3, and not
@@ -543,7 +537,7 @@
 
   void ReceiveWebTransportSettings() {
     SettingsFrame settings;
-    settings.values[SETTINGS_H3_DATAGRAM] = 1;
+    settings.values[SETTINGS_H3_DATAGRAM_DRAFT04] = 1;
     settings.values[SETTINGS_WEBTRANS_DRAFT00] = 1;
     std::string data =
         std::string(1, kControlStream) + EncodeSettings(settings);
@@ -567,8 +561,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::kDraft00) {
+      stream->OnCapsule(
+          Capsule::RegisterDatagramNoContext(DatagramFormatType::WEBTRANSPORT));
+    }
     WebTransportHttp3* web_transport =
         session_.GetWebTransportSession(session_id);
     ASSERT_TRUE(web_transport != nullptr);
@@ -588,6 +588,11 @@
     session_.OnStreamFrame(frame);
   }
 
+  void TestHttpDatagramSetting(HttpDatagramSupport local_support,
+                               HttpDatagramSupport remote_support,
+                               HttpDatagramSupport expected_support,
+                               bool expected_datagram_supported);
+
   MockQuicConnectionHelper helper_;
   MockAlarmFactory alarm_factory_;
   StrictMock<MockQuicConnection>* connection_;
@@ -602,8 +607,7 @@
       : QuicSpdySessionTestBase(Perspective::IS_SERVER) {}
 };
 
-INSTANTIATE_TEST_SUITE_P(Tests,
-                         QuicSpdySessionTestServer,
+INSTANTIATE_TEST_SUITE_P(Tests, QuicSpdySessionTestServer,
                          ::testing::ValuesIn(AllSupportedVersions()),
                          ::testing::PrintToStringParamName());
 
@@ -1849,8 +1853,7 @@
       : QuicSpdySessionTestBase(Perspective::IS_CLIENT) {}
 };
 
-INSTANTIATE_TEST_SUITE_P(Tests,
-                         QuicSpdySessionTestClient,
+INSTANTIATE_TEST_SUITE_P(Tests, QuicSpdySessionTestClient,
                          ::testing::ValuesIn(AllSupportedVersions()),
                          ::testing::PrintToStringParamName());
 
@@ -3411,16 +3414,32 @@
   EXPECT_EQ("multiple SETTINGS frames", error.value());
 }
 
-TEST_P(QuicSpdySessionTestClient, H3DatagramSetting) {
+void QuicSpdySessionTestBase::TestHttpDatagramSetting(
+    HttpDatagramSupport local_support, HttpDatagramSupport remote_support,
+    HttpDatagramSupport expected_support, bool expected_datagram_supported) {
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_should_negotiate_h3_datagram(true);
+  session_.set_local_http_datagram_support(local_support);
   // 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;
+  switch (remote_support) {
+    case HttpDatagramSupport::kNone:
+      break;
+    case HttpDatagramSupport::kDraft00:
+      settings.values[SETTINGS_H3_DATAGRAM_DRAFT00] = 1;
+      break;
+    case HttpDatagramSupport::kDraft04:
+      settings.values[SETTINGS_H3_DATAGRAM_DRAFT04] = 1;
+      break;
+    case HttpDatagramSupport::kDraft00And04:
+      settings.values[SETTINGS_H3_DATAGRAM_DRAFT00] = 1;
+      settings.values[SETTINGS_H3_DATAGRAM_DRAFT04] = 1;
+      break;
+  }
   std::string data = std::string(1, kControlStream) + EncodeSettings(settings);
   QuicStreamId stream_id =
       GetNthServerInitiatedUnidirectionalStreamId(transport_version(), 3);
@@ -3430,15 +3449,88 @@
   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_.h3_datagram_supported());
+  EXPECT_EQ(session_.http_datagram_support(), expected_support);
+  EXPECT_EQ(session_.SupportsH3Datagram(), expected_datagram_supported);
+}
+
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingLocal00Remote00) {
+  TestHttpDatagramSetting(
+      /*local_support=*/HttpDatagramSupport::kDraft00,
+      /*remote_support=*/HttpDatagramSupport::kDraft00,
+      /*expected_support=*/HttpDatagramSupport::kDraft00,
+      /*expected_datagram_supported=*/true);
+}
+
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingLocal00Remote04) {
+  TestHttpDatagramSetting(
+      /*local_support=*/HttpDatagramSupport::kDraft00,
+      /*remote_support=*/HttpDatagramSupport::kDraft04,
+      /*expected_support=*/HttpDatagramSupport::kNone,
+      /*expected_datagram_supported=*/false);
+}
+
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingLocal00Remote00And04) {
+  TestHttpDatagramSetting(
+      /*local_support=*/HttpDatagramSupport::kDraft00,
+      /*remote_support=*/HttpDatagramSupport::kDraft00And04,
+      /*expected_support=*/HttpDatagramSupport::kDraft00,
+      /*expected_datagram_supported=*/true);
+}
+
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingLocal04Remote00) {
+  TestHttpDatagramSetting(
+      /*local_support=*/HttpDatagramSupport::kDraft04,
+      /*remote_support=*/HttpDatagramSupport::kDraft00,
+      /*expected_support=*/HttpDatagramSupport::kNone,
+      /*expected_datagram_supported=*/false);
+}
+
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingLocal04Remote04) {
+  TestHttpDatagramSetting(
+      /*local_support=*/HttpDatagramSupport::kDraft04,
+      /*remote_support=*/HttpDatagramSupport::kDraft04,
+      /*expected_support=*/HttpDatagramSupport::kDraft04,
+      /*expected_datagram_supported=*/true);
+}
+
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingLocal04Remote00And04) {
+  TestHttpDatagramSetting(
+      /*local_support=*/HttpDatagramSupport::kDraft04,
+      /*remote_support=*/HttpDatagramSupport::kDraft00And04,
+      /*expected_support=*/HttpDatagramSupport::kDraft04,
+      /*expected_datagram_supported=*/true);
+}
+
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingLocal00And04Remote00) {
+  TestHttpDatagramSetting(
+      /*local_support=*/HttpDatagramSupport::kDraft00And04,
+      /*remote_support=*/HttpDatagramSupport::kDraft00,
+      /*expected_support=*/HttpDatagramSupport::kDraft00,
+      /*expected_datagram_supported=*/true);
+}
+
+TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingLocal00And04Remote04) {
+  TestHttpDatagramSetting(
+      /*local_support=*/HttpDatagramSupport::kDraft00And04,
+      /*remote_support=*/HttpDatagramSupport::kDraft04,
+      /*expected_support=*/HttpDatagramSupport::kDraft04,
+      /*expected_datagram_supported=*/true);
+}
+
+TEST_P(QuicSpdySessionTestClient,
+       HttpDatagramSettingLocal00And04Remote00And04) {
+  TestHttpDatagramSetting(
+      /*local_support=*/HttpDatagramSupport::kDraft00And04,
+      /*remote_support=*/HttpDatagramSupport::kDraft00And04,
+      /*expected_support=*/HttpDatagramSupport::kDraft04,
+      /*expected_datagram_supported=*/true);
 }
 
 TEST_P(QuicSpdySessionTestClient, WebTransportSetting) {
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_should_negotiate_h3_datagram(true);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
   session_.set_supports_webtransport(true);
 
   EXPECT_FALSE(session_.SupportsWebTransport());
@@ -3451,7 +3543,7 @@
   CompleteHandshake();
 
   SettingsFrame server_settings;
-  server_settings.values[SETTINGS_H3_DATAGRAM] = 1;
+  server_settings.values[SETTINGS_H3_DATAGRAM_DRAFT04] = 1;
   server_settings.values[SETTINGS_WEBTRANS_DRAFT00] = 1;
   std::string data =
       std::string(1, kControlStream) + EncodeSettings(server_settings);
@@ -3468,7 +3560,7 @@
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_should_negotiate_h3_datagram(true);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
   session_.set_supports_webtransport(true);
 
   EXPECT_FALSE(session_.SupportsWebTransport());
@@ -3481,7 +3573,7 @@
   CompleteHandshake();
 
   SettingsFrame server_settings;
-  server_settings.values[SETTINGS_H3_DATAGRAM] = 1;
+  server_settings.values[SETTINGS_H3_DATAGRAM_DRAFT04] = 1;
   server_settings.values[SETTINGS_WEBTRANS_DRAFT00] = 0;
   std::string data =
       std::string(1, kControlStream) + EncodeSettings(server_settings);
@@ -3498,7 +3590,7 @@
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_should_negotiate_h3_datagram(true);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
   session_.set_supports_webtransport(true);
 
   EXPECT_FALSE(session_.SupportsWebTransport());
@@ -3515,7 +3607,7 @@
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_should_negotiate_h3_datagram(true);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
   session_.set_supports_webtransport(true);
 
   CompleteHandshake();
@@ -3548,7 +3640,7 @@
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_should_negotiate_h3_datagram(true);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
   session_.set_supports_webtransport(true);
 
   CompleteHandshake();
@@ -3589,7 +3681,7 @@
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_should_negotiate_h3_datagram(true);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
   session_.set_supports_webtransport(true);
 
   CompleteHandshake();
diff --git a/quic/core/http/quic_spdy_stream.cc b/quic/core/http/quic_spdy_stream.cc
index c33e763..e9df950 100644
--- a/quic/core/http/quic_spdy_stream.cc
+++ b/quic/core/http/quic_spdy_stream.cc
@@ -13,8 +13,10 @@
 #include "absl/strings/numbers.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "quic/core/http/capsule.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"
@@ -129,13 +131,11 @@
   }
 
   void OnWebTransportStreamFrameType(
-      QuicByteCount header_length,
-      WebTransportSessionId session_id) override {
+      QuicByteCount header_length, WebTransportSessionId session_id) override {
     stream_->OnWebTransportStreamFrameType(header_length, session_id);
   }
 
-  bool OnUnknownFrameStart(uint64_t frame_type,
-                           QuicByteCount header_length,
+  bool OnUnknownFrameStart(uint64_t frame_type, QuicByteCount header_length,
                            QuicByteCount payload_length) override {
     return stream_->OnUnknownFrameStart(frame_type, header_length,
                                         payload_length);
@@ -249,8 +249,7 @@
 QuicSpdyStream::~QuicSpdyStream() {}
 
 size_t QuicSpdyStream::WriteHeaders(
-    SpdyHeaderBlock header_block,
-    bool fin,
+    SpdyHeaderBlock header_block, bool fin,
     QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener) {
   if (!AssertNotWebTransportDataStream("writing headers")) {
     return 0;
@@ -286,6 +285,16 @@
     SetFinSent();
     CloseWriteSide();
   }
+
+  if (web_transport_ != nullptr &&
+      session()->perspective() == Perspective::IS_CLIENT) {
+    // This will send a capsule and therefore needs to happen after headers have
+    // been sent.
+    RegisterHttp3DatagramContextId(
+        web_transport_->context_id(), DatagramFormatType::WEBTRANSPORT,
+        /*format_additional_data=*/absl::string_view(), web_transport_.get());
+  }
+
   return bytes_written;
 }
 
@@ -355,8 +364,7 @@
   return bytes_written;
 }
 
-QuicConsumedData QuicSpdyStream::WritevBody(const struct iovec* iov,
-                                            int count,
+QuicConsumedData QuicSpdyStream::WritevBody(const struct iovec* iov, int count,
                                             bool fin) {
   QuicMemSliceStorage storage(
       iov, count,
@@ -399,8 +407,7 @@
 }
 
 QuicConsumedData QuicSpdyStream::WriteBodySlices(
-    absl::Span<QuicMemSlice> slices,
-    bool fin) {
+    absl::Span<QuicMemSlice> slices, bool fin) {
   if (!VersionUsesHttp3(transport_version()) || slices.empty()) {
     return WriteMemSlices(slices, fin);
   }
@@ -459,9 +466,7 @@
   return body_manager_.HasBytesToRead();
 }
 
-void QuicSpdyStream::MarkTrailersConsumed() {
-  trailers_consumed_ = true;
-}
+void QuicSpdyStream::MarkTrailersConsumed() { trailers_consumed_ = true; }
 
 uint64_t QuicSpdyStream::total_body_bytes_read() const {
   if (VersionUsesHttp3(transport_version())) {
@@ -483,14 +488,14 @@
   }
 
   if (body_manager_.HasBytesToRead()) {
-    OnBodyAvailable();
+    HandleBodyAvailable();
     return;
   }
 
   if (sequencer()->IsClosed() &&
       !on_body_available_called_because_sequencer_is_closed_) {
     on_body_available_called_because_sequencer_is_closed_ = true;
-    OnBodyAvailable();
+    HandleBodyAvailable();
   }
 }
 
@@ -501,8 +506,7 @@
   SetPriority(precedence);
 }
 
-void QuicSpdyStream::OnStreamHeaderList(bool fin,
-                                        size_t frame_len,
+void QuicSpdyStream::OnStreamHeaderList(bool fin, size_t frame_len,
                                         const QuicHeaderList& header_list) {
   if (!spdy_session()->user_agent_id().has_value()) {
     std::string uaid;
@@ -595,14 +599,10 @@
   spdy_session_->WriteHttp3PriorityUpdate(priority_update);
 }
 
-void QuicSpdyStream::OnHeadersTooLarge() {
-  Reset(QUIC_HEADERS_TOO_LARGE);
-}
+void QuicSpdyStream::OnHeadersTooLarge() { Reset(QUIC_HEADERS_TOO_LARGE); }
 
 void QuicSpdyStream::OnInitialHeadersComplete(
-    bool fin,
-    size_t /*frame_len*/,
-    const QuicHeaderList& header_list) {
+    bool fin, size_t /*frame_len*/, const QuicHeaderList& header_list) {
   // TODO(b/134706391): remove |fin| argument.
   headers_decompressed_ = true;
   header_list_ = header_list;
@@ -628,8 +628,7 @@
 }
 
 void QuicSpdyStream::OnPromiseHeaderList(
-    QuicStreamId /* promised_id */,
-    size_t /* frame_len */,
+    QuicStreamId /* promised_id */, size_t /* frame_len */,
     const QuicHeaderList& /*header_list */) {
   // To be overridden in QuicSpdyClientStream.  Not supported on
   // server side.
@@ -638,9 +637,7 @@
 }
 
 void QuicSpdyStream::OnTrailingHeadersComplete(
-    bool fin,
-    size_t /*frame_len*/,
-    const QuicHeaderList& header_list) {
+    bool fin, size_t /*frame_len*/, const QuicHeaderList& header_list) {
   // TODO(b/134706391): remove |fin| argument.
   QUICHE_DCHECK(!trailers_decompressed_);
   if (!VersionUsesHttp3(transport_version()) && fin_received()) {
@@ -780,7 +777,7 @@
   }
 
   if (!VersionUsesHttp3(transport_version())) {
-    OnBodyAvailable();
+    HandleBodyAvailable();
     return;
   }
 
@@ -825,20 +822,20 @@
     }
   }
 
-  // Do not call OnBodyAvailable() until headers are consumed.
+  // Do not call HandleBodyAvailable() until headers are consumed.
   if (!FinishedReadingHeaders()) {
     return;
   }
 
   if (body_manager_.HasBytesToRead()) {
-    OnBodyAvailable();
+    HandleBodyAvailable();
     return;
   }
 
   if (sequencer()->IsClosed() &&
       !on_body_available_called_because_sequencer_is_closed_) {
     on_body_available_called_because_sequencer_is_closed_ = true;
-    OnBodyAvailable();
+    HandleBodyAvailable();
   }
 }
 
@@ -1000,8 +997,7 @@
 }
 
 QuicByteCount QuicSpdyStream::GetNumFrameHeadersInInterval(
-    QuicStreamOffset offset,
-    QuicByteCount data_length) const {
+    QuicStreamOffset offset, QuicByteCount data_length) const {
   QuicByteCount header_acked_length = 0;
   QuicIntervalSet<QuicStreamOffset> newly_acked(offset, offset + data_length);
   newly_acked.Intersection(unacked_frame_headers_offsets_);
@@ -1072,8 +1068,7 @@
 }
 
 void QuicSpdyStream::OnWebTransportStreamFrameType(
-    QuicByteCount header_length,
-    WebTransportSessionId session_id) {
+    QuicByteCount header_length, WebTransportSessionId session_id) {
   QUIC_DVLOG(1) << ENDPOINT << " Received WEBTRANSPORT_STREAM on stream "
                 << id() << " for session " << session_id;
   sequencer()->MarkConsumed(header_length);
@@ -1125,13 +1120,10 @@
   return true;
 }
 
-bool QuicSpdyStream::OnUnknownFrameEnd() {
-  return true;
-}
+bool QuicSpdyStream::OnUnknownFrameEnd() { return true; }
 
 size_t QuicSpdyStream::WriteHeadersImpl(
-    spdy::SpdyHeaderBlock header_block,
-    bool fin,
+    spdy::SpdyHeaderBlock header_block, bool fin,
     QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener) {
   if (!VersionUsesHttp3(transport_version())) {
     return spdy_session_->WriteHeadersOnHeadersStream(
@@ -1217,6 +1209,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;
       }
@@ -1228,23 +1227,33 @@
     }
   }
 
-  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.
+  // a REGISTER_DATAGRAM_NO_CONTEXT capsule.
   // TODO(b/181256914) remove this when we remove support for
   // draft-ietf-masque-h3-datagram-00 in favor of later drafts.
-  RegisterHttp3DatagramContextId(/*context_id=*/absl::nullopt,
-                                 Http3DatagramContextExtensions(),
-                                 web_transport_.get());
+  RegisterHttp3DatagramContextId(
+      /*context_id=*/absl::nullopt, DatagramFormatType::WEBTRANSPORT,
+      /*format_additional_data=*/absl::string_view(), web_transport_.get());
 }
 
 void QuicSpdyStream::MaybeProcessSentWebTransportHeaders(
@@ -1266,14 +1275,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() {
@@ -1329,11 +1336,101 @@
 }
 
 QuicSpdyStream::WebTransportDataStream::WebTransportDataStream(
-    QuicSpdyStream* stream,
-    WebTransportSessionId session_id)
+    QuicSpdyStream* stream, WebTransportSessionId session_id)
     : session_id(session_id),
       adapter(stream->spdy_session_, stream, stream->sequencer()) {}
 
+void QuicSpdyStream::HandleReceivedDatagram(
+    absl::optional<QuicDatagramContextId> context_id,
+    absl::string_view payload) {
+  Http3DatagramVisitor* visitor;
+  if (context_id.has_value()) {
+    auto it = datagram_context_visitors_.find(context_id.value());
+    if (it == datagram_context_visitors_.end()) {
+      QUIC_DLOG(ERROR) << ENDPOINT
+                       << "Received datagram without any visitor for context "
+                       << context_id.value();
+      return;
+    }
+    visitor = it->second;
+  } else {
+    if (datagram_no_context_visitor_ == nullptr) {
+      QUIC_DLOG(ERROR)
+          << ENDPOINT << "Received datagram without any visitor for no context";
+      return;
+    }
+    visitor = datagram_no_context_visitor_;
+  }
+  visitor->OnHttp3Datagram(id(), context_id, payload);
+}
+
+bool QuicSpdyStream::OnCapsule(const Capsule& capsule) {
+  QUIC_DLOG(INFO) << ENDPOINT << "Stream " << id() << " received capsule "
+                  << capsule;
+  if (!headers_decompressed_) {
+    QUIC_PEER_BUG(capsule before headers)
+        << ENDPOINT << "Stream " << id() << " received capsule " << capsule
+        << " before headers";
+    return false;
+  }
+  switch (capsule.capsule_type()) {
+    case CapsuleType::DATAGRAM: {
+      HandleReceivedDatagram(capsule.datagram_capsule().context_id,
+                             capsule.datagram_capsule().http_datagram_payload);
+    } break;
+    case CapsuleType::REGISTER_DATAGRAM_CONTEXT:
+      if (datagram_registration_visitor_ == nullptr) {
+        QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << capsule
+                         << " without any registration visitor";
+        return false;
+      }
+      datagram_registration_visitor_->OnContextReceived(
+          id(), capsule.register_datagram_context_capsule().context_id,
+          capsule.register_datagram_context_capsule().format_type,
+          capsule.register_datagram_context_capsule().format_additional_data);
+      break;
+    case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT:
+      if (datagram_registration_visitor_ == nullptr) {
+        QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << capsule
+                         << " without any registration visitor";
+        return false;
+      }
+      datagram_registration_visitor_->OnContextReceived(
+          id(), /*context_id=*/absl::nullopt,
+          capsule.register_datagram_no_context_capsule().format_type,
+          capsule.register_datagram_no_context_capsule()
+              .format_additional_data);
+      break;
+    case CapsuleType::CLOSE_DATAGRAM_CONTEXT:
+      if (datagram_registration_visitor_ == nullptr) {
+        QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << capsule
+                         << " without any registration visitor";
+        return false;
+      }
+      datagram_registration_visitor_->OnContextClosed(
+          id(), capsule.close_datagram_context_capsule().context_id,
+          capsule.close_datagram_context_capsule().close_code,
+          capsule.close_datagram_context_capsule().close_details);
+      break;
+  }
+  return true;
+}
+
+void QuicSpdyStream::OnCapsuleParseFailure(const std::string& error_message) {
+  QUIC_DLOG(ERROR) << ENDPOINT << "Capsule parse failure: " << error_message;
+  Reset(QUIC_BAD_APPLICATION_PAYLOAD);
+}
+
+void QuicSpdyStream::WriteCapsule(const Capsule& capsule) {
+  QUIC_DLOG(INFO) << ENDPOINT << "Stream " << id() << " sending capsule "
+                  << capsule;
+  QuicBuffer serialized_capsule = SerializeCapsule(
+      capsule,
+      spdy_session_->connection()->helper()->GetStreamSendBufferAllocator());
+  QUICHE_DCHECK_GT(serialized_capsule.size(), 0u);
+  WriteOrBufferBody(serialized_capsule.AsStringView(), /*fin=*/false);
+}
+
 MessageStatus QuicSpdyStream::SendHttp3Datagram(
     absl::optional<QuicDatagramContextId> context_id,
     absl::string_view payload) {
@@ -1349,8 +1446,15 @@
         << ENDPOINT << "Null datagram registration visitor for" << id();
     return;
   }
+  if (datagram_registration_visitor_ != nullptr) {
+    QUIC_BUG(double datagram registration visitor)
+        << ENDPOINT << "Double datagram registration visitor for" << id();
+    return;
+  }
   QUIC_DLOG(INFO) << ENDPOINT << "Registering datagram stream ID " << id();
   datagram_registration_visitor_ = visitor;
+  QUICHE_DCHECK(!capsule_parser_);
+  capsule_parser_.reset(new CapsuleParser(this));
 }
 
 void QuicSpdyStream::UnregisterHttp3DatagramRegistrationVisitor() {
@@ -1374,24 +1478,27 @@
 
 void QuicSpdyStream::RegisterHttp3DatagramContextId(
     absl::optional<QuicDatagramContextId> context_id,
-    const Http3DatagramContextExtensions& /*extensions*/,
+    DatagramFormatType format_type, absl::string_view format_additional_data,
     Http3DatagramVisitor* visitor) {
   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)
@@ -1403,28 +1510,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;
+    }
+    capsule_parser_->set_datagram_context_id_present(true);
+  } 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;
+    capsule_parser_->set_datagram_context_id_present(false);
   }
-  // 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::kDraft04) {
+    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) {
+        WriteCapsule(Capsule::RegisterDatagramContext(
+            context_id.value(), format_type, format_additional_data));
+      }
+    } else if (is_client) {
+      WriteCapsule(Capsule::RegisterDatagramNoContext(format_type,
+                                                      format_additional_data));
+    }
   }
-  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(
@@ -1432,26 +1556,31 @@
   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::kDraft04 &&
+      context_id.has_value()) {
+    WriteCapsule(Capsule::CloseDatagramContext(context_id.value()));
+  }
 }
 
 void QuicSpdyStream::MoveHttp3DatagramContextIdRegistration(
@@ -1460,12 +1589,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,
@@ -1496,7 +1626,6 @@
 void QuicSpdyStream::OnDatagramReceived(QuicDataReader* reader) {
   absl::optional<QuicDatagramContextId> context_id;
   const bool context_id_present = !datagram_context_visitors_.empty();
-  Http3DatagramVisitor* visitor;
   if (context_id_present) {
     QuicDatagramContextId parsed_context_id;
     if (!reader->ReadVarInt62(&parsed_context_id)) {
@@ -1506,28 +1635,9 @@
       return;
     }
     context_id = parsed_context_id;
-    auto it = datagram_context_visitors_.find(parsed_context_id);
-    if (it == datagram_context_visitors_.end()) {
-      // TODO(b/181256914) buffer unknown HTTP/3 datagrams for a short
-      // period of time in case they were reordered.
-      QUIC_DLOG(ERROR) << "Received unknown HTTP/3 datagram context ID "
-                       << parsed_context_id << " on stream ID " << id();
-      return;
-    }
-    visitor = it->second;
-  } else {
-    if (datagram_no_context_visitor_ == nullptr) {
-      // TODO(b/181256914) buffer unknown HTTP/3 datagrams for a short
-      // period of time in case they were reordered.
-      QUIC_DLOG(ERROR)
-          << "Received HTTP/3 datagram without any registrations on stream ID "
-          << id();
-      return;
-    }
-    visitor = datagram_no_context_visitor_;
   }
   absl::string_view payload = reader->ReadRemainingPayload();
-  visitor->OnHttp3Datagram(id(), context_id, payload);
+  HandleReceivedDatagram(context_id, payload);
 }
 
 void QuicSpdyStream::RegisterHttp3DatagramFlowId(QuicDatagramStreamId flow_id) {
@@ -1535,5 +1645,29 @@
   spdy_session_->RegisterHttp3DatagramFlowId(datagram_flow_id_.value(), id());
 }
 
+void QuicSpdyStream::HandleBodyAvailable() {
+  if (!capsule_parser_) {
+    OnBodyAvailable();
+    return;
+  }
+  while (body_manager_.HasBytesToRead()) {
+    iovec iov;
+    int num_iov = GetReadableRegions(&iov, /*iov_len=*/1);
+    if (num_iov == 0) {
+      break;
+    }
+    if (!capsule_parser_->IngestCapsuleFragment(absl::string_view(
+            reinterpret_cast<const char*>(iov.iov_base), iov.iov_len))) {
+      break;
+    }
+    MarkConsumed(iov.iov_len);
+  }
+  // If we received a FIN, make sure that there isn't a partial capsule buffered
+  // in the capsule parser.
+  if (sequencer()->IsClosed()) {
+    capsule_parser_->ErrorIfThereIsRemainingBufferedData();
+  }
+}
+
 #undef ENDPOINT  // undef for jumbo builds
 }  // namespace quic
diff --git a/quic/core/http/quic_spdy_stream.h b/quic/core/http/quic_spdy_stream.h
index 65b3e5e..21dab65 100644
--- a/quic/core/http/quic_spdy_stream.h
+++ b/quic/core/http/quic_spdy_stream.h
@@ -19,6 +19,7 @@
 #include "absl/base/attributes.h"
 #include "absl/strings/string_view.h"
 #include "absl/types/span.h"
+#include "quic/core/http/capsule.h"
 #include "quic/core/http/http_decoder.h"
 #include "quic/core/http/http_encoder.h"
 #include "quic/core/http/quic_header_list.h"
@@ -47,11 +48,10 @@
 class QuicSpdySession;
 class WebTransportHttp3;
 
-class QUIC_EXPORT_PRIVATE Http3DatagramContextExtensions {};
-
 // A QUIC stream that can send and receive HTTP2 (SPDY) headers.
 class QUIC_EXPORT_PRIVATE QuicSpdyStream
     : public QuicStream,
+      public CapsuleParser::Visitor,
       public QpackDecodedHeadersAccumulator::Visitor {
  public:
   // Visitor receives callbacks from the stream.
@@ -72,8 +72,7 @@
     virtual ~Visitor() {}
   };
 
-  QuicSpdyStream(QuicStreamId id,
-                 QuicSpdySession* spdy_session,
+  QuicSpdyStream(QuicStreamId id, QuicSpdySession* spdy_session,
                  StreamType type);
   QuicSpdyStream(PendingStream* pending, QuicSpdySession* spdy_session);
   QuicSpdyStream(const QuicSpdyStream&) = delete;
@@ -94,14 +93,12 @@
   // Called by the session when decompressed headers have been completely
   // delivered to this stream.  If |fin| is true, then this stream
   // should be closed; no more data will be sent by the peer.
-  virtual void OnStreamHeaderList(bool fin,
-                                  size_t frame_len,
+  virtual void OnStreamHeaderList(bool fin, size_t frame_len,
                                   const QuicHeaderList& header_list);
 
   // Called by the session when decompressed push promise headers have
   // been completely delivered to this stream.
-  virtual void OnPromiseHeaderList(QuicStreamId promised_id,
-                                   size_t frame_len,
+  virtual void OnPromiseHeaderList(QuicStreamId promised_id, size_t frame_len,
                                    const QuicHeaderList& header_list);
 
   // Called by the session when a PRIORITY frame has been been received for this
@@ -126,8 +123,7 @@
   // number of bytes sent, including data sent on the encoder stream when using
   // QPACK.
   virtual size_t WriteHeaders(
-      spdy::SpdyHeaderBlock header_block,
-      bool fin,
+      spdy::SpdyHeaderBlock header_block, bool fin,
       QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener);
 
   // Sends |data| to the peer, or buffers if it can't be sent immediately.
@@ -142,10 +138,8 @@
       QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener);
 
   // Override to report newly acked bytes via ack_listener_.
-  bool OnStreamFrameAcked(QuicStreamOffset offset,
-                          QuicByteCount data_length,
-                          bool fin_acked,
-                          QuicTime::Delta ack_delay_time,
+  bool OnStreamFrameAcked(QuicStreamOffset offset, QuicByteCount data_length,
+                          bool fin_acked, QuicTime::Delta ack_delay_time,
                           QuicTime receive_timestamp,
                           QuicByteCount* newly_acked_length) override;
 
@@ -252,6 +246,10 @@
   // rejected due to buffer being full.  |write_size| must be non-zero.
   bool CanWriteNewBodyData(QuicByteCount write_size) const;
 
+  // From CapsuleParser::Visitor.
+  bool OnCapsule(const Capsule& capsule) override;
+  void OnCapsuleParseFailure(const std::string& error_message) override;
+
   // Sends an HTTP/3 datagram. The stream and context IDs are not part of
   // |payload|.
   MessageStatus SendHttp3Datagram(
@@ -281,7 +279,8 @@
     virtual void OnContextReceived(
         QuicStreamId stream_id,
         absl::optional<QuicDatagramContextId> context_id,
-        const Http3DatagramContextExtensions& extensions) = 0;
+        DatagramFormatType format_type,
+        absl::string_view format_additional_data) = 0;
 
     // Called when a CLOSE_DATAGRAM_CONTEXT capsule is received. Note that this
     // contains the stream ID even if flow IDs from
@@ -289,7 +288,7 @@
     virtual void OnContextClosed(
         QuicStreamId stream_id,
         absl::optional<QuicDatagramContextId> context_id,
-        const Http3DatagramContextExtensions& extensions) = 0;
+        ContextCloseCode close_code, absl::string_view close_details) = 0;
   };
 
   // Registers |visitor| to receive HTTP/3 datagram context registrations. This
@@ -315,7 +314,7 @@
   // present, or always absent.
   void RegisterHttp3DatagramContextId(
       absl::optional<QuicDatagramContextId> context_id,
-      const Http3DatagramContextExtensions& extensions,
+      DatagramFormatType format_type, absl::string_view format_additional_data,
       Http3DatagramVisitor* visitor);
 
   // Unregisters an HTTP/3 datagram context ID. Must be called on a previously
@@ -345,15 +344,12 @@
   // reset the stream.
   virtual void OnHeadersTooLarge();
 
-  virtual void OnInitialHeadersComplete(bool fin,
-                                        size_t frame_len,
+  virtual void OnInitialHeadersComplete(bool fin, size_t frame_len,
                                         const QuicHeaderList& header_list);
-  virtual void OnTrailingHeadersComplete(bool fin,
-                                         size_t frame_len,
+  virtual void OnTrailingHeadersComplete(bool fin, size_t frame_len,
                                          const QuicHeaderList& header_list);
   virtual size_t WriteHeadersImpl(
-      spdy::SpdyHeaderBlock header_block,
-      bool fin,
+      spdy::SpdyHeaderBlock header_block, bool fin,
       QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener);
 
   Visitor* visitor() { return visitor_; }
@@ -392,8 +388,7 @@
   bool OnHeadersFrameEnd();
   void OnWebTransportStreamFrameType(QuicByteCount header_length,
                                      WebTransportSessionId session_id);
-  bool OnUnknownFrameStart(uint64_t frame_type,
-                           QuicByteCount header_length,
+  bool OnUnknownFrameStart(uint64_t frame_type, QuicByteCount header_length,
                            QuicByteCount payload_length);
   bool OnUnknownFramePayload(absl::string_view payload);
   bool OnUnknownFrameEnd();
@@ -411,6 +406,17 @@
   ABSL_MUST_USE_RESULT bool WriteDataFrameHeader(QuicByteCount data_length,
                                                  bool force_write);
 
+  // Simply calls OnBodyAvailable() unless capsules are in use, in which case
+  // pass the capsule fragments to the capsule manager.
+  void HandleBodyAvailable();
+
+  // Called when a datagram frame or capsule is received.
+  void HandleReceivedDatagram(absl::optional<QuicDatagramContextId> context_id,
+                              absl::string_view payload);
+
+  // Writes |capsule| onto the DATA stream.
+  void WriteCapsule(const Capsule& capsule);
+
   QuicSpdySession* spdy_session_;
 
   bool on_body_available_called_because_sequencer_is_closed_;
@@ -451,6 +457,8 @@
   // the sequencer each time new stream data is processed.
   QuicSpdyStreamBodyManager body_manager_;
 
+  std::unique_ptr<CapsuleParser> capsule_parser_;
+
   // Sequencer offset keeping track of how much data HttpDecoder has processed.
   // Initial value is zero for fresh streams, or sequencer()->NumBytesConsumed()
   // at time of construction if a PendingStream is converted to account for the
diff --git a/quic/core/http/quic_spdy_stream_test.cc b/quic/core/http/quic_spdy_stream_test.cc
index 63a77ce..594b92c 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"
@@ -256,16 +257,16 @@
   bool ShouldNegotiateWebTransport() override { return enable_webtransport_; }
   void EnableWebTransport() { enable_webtransport_ = true; }
 
-  bool ShouldNegotiateHttp3Datagram() override {
-    return should_negotiate_h3_datagram_;
+  HttpDatagramSupport LocalHttpDatagramSupport() override {
+    return local_http_datagram_support_;
   }
-  void set_should_negotiate_h3_datagram(bool value) {
-    should_negotiate_h3_datagram_ = value;
+  void set_local_http_datagram_support(HttpDatagramSupport value) {
+    local_http_datagram_support_ = value;
   }
 
  private:
   bool enable_webtransport_ = false;
-  bool should_negotiate_h3_datagram_ = false;
+  HttpDatagramSupport local_http_datagram_support_ = HttpDatagramSupport::kNone;
   StrictMock<TestCryptoStream> crypto_stream_;
 };
 
@@ -3029,15 +3030,17 @@
   }
 }
 
-TEST_P(QuicSpdyStreamTest, ProcessOutgoingWebTransportHeaders) {
+TEST_P(QuicSpdyStreamTest, ProcessOutgoingWebTransportHeadersDatagramDraft00) {
   if (!UsesHttp3()) {
     return;
   }
 
   InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
-  session_->set_should_negotiate_h3_datagram(true);
+  session_->set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
   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(), _, _, _, _, _))
@@ -3052,15 +3055,68 @@
   EXPECT_EQ(stream_->id(), stream_->web_transport()->id());
 }
 
-TEST_P(QuicSpdyStreamTest, ProcessIncomingWebTransportHeaders) {
+TEST_P(QuicSpdyStreamTest, ProcessOutgoingWebTransportHeadersDatagramDraft04) {
+  if (!UsesHttp3()) {
+    return;
+  }
+
+  InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
+  session_->set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
+  session_->EnableWebTransport();
+  QuicSpdySessionPeer::EnableWebTransport(session_.get());
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft04);
+
+  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, ProcessIncomingWebTransportHeadersDatagramDraft04) {
   if (!UsesHttp3()) {
     return;
   }
 
   Initialize(kShouldProcessData);
-  session_->set_should_negotiate_h3_datagram(true);
+  session_->set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
   session_->EnableWebTransport();
-  QuicSpdySessionPeer::EnableWebTransport(*session_);
+  QuicSpdySessionPeer::EnableWebTransport(session_.get());
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft04);
+
+  headers_[":method"] = "CONNECT";
+  headers_[":protocol"] = "webtransport";
+
+  stream_->OnStreamHeadersPriority(
+      spdy::SpdyStreamPrecedence(kV3HighestPriority));
+  ProcessHeaders(false, headers_);
+  stream_->OnCapsule(
+      Capsule::RegisterDatagramNoContext(DatagramFormatType::WEBTRANSPORT));
+  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, ProcessIncomingWebTransportHeadersDatagramDraft00) {
+  if (!UsesHttp3()) {
+    return;
+  }
+
+  Initialize(kShouldProcessData);
+  session_->set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
+  session_->EnableWebTransport();
+  QuicSpdySessionPeer::EnableWebTransport(session_.get());
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft00);
 
   headers_[":method"] = "CONNECT";
   headers_[":protocol"] = "webtransport";
@@ -3085,9 +3141,11 @@
   // draft-ietf-masque-h3-datagram-00 in favor of later drafts.
 
   Initialize(kShouldProcessData);
-  session_->set_should_negotiate_h3_datagram(true);
+  session_->set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
   session_->EnableWebTransport();
-  QuicSpdySessionPeer::EnableWebTransport(*session_);
+  QuicSpdySessionPeer::EnableWebTransport(session_.get());
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft00);
 
   headers_[":method"] = "CONNECT";
   headers_[":protocol"] = "webtransport";
@@ -3131,19 +3189,20 @@
   stream_->UnregisterHttp3DatagramRegistrationVisitor();
 }
 
-TEST_P(QuicSpdyStreamTest, H3DatagramRegistrationWithoutContext) {
+TEST_P(QuicSpdyStreamTest, HttpDatagramRegistrationWithoutContextDraft00) {
   if (!UsesHttp3()) {
     return;
   }
-  Initialize(kShouldProcessData);
-  session_->set_should_negotiate_h3_datagram(true);
-  QuicSpdySessionPeer::SetH3DatagramSupported(session_.get(), true);
+  InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
+  session_->set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft00);
   session_->RegisterHttp3DatagramFlowId(stream_->id(), stream_->id());
   ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor>
       h3_datagram_registration_visitor;
   SavingHttp3DatagramVisitor h3_datagram_visitor;
   absl::optional<QuicDatagramContextId> context_id;
-  Http3DatagramContextExtensions extensions;
+  absl::string_view format_additional_data;
   ASSERT_EQ(QuicDataWriter::GetVarInt62Len(stream_->id()), 1);
   std::array<char, 256> datagram;
   datagram[0] = stream_->id();
@@ -3152,8 +3211,9 @@
   }
   stream_->RegisterHttp3DatagramRegistrationVisitor(
       &h3_datagram_registration_visitor);
-  stream_->RegisterHttp3DatagramContextId(context_id, extensions,
-                                          &h3_datagram_visitor);
+  stream_->RegisterHttp3DatagramContextId(
+      context_id, DatagramFormatType::UDP_PAYLOAD, format_additional_data,
+      &h3_datagram_visitor);
   session_->OnMessageReceived(
       absl::string_view(datagram.data(), datagram.size()));
   EXPECT_THAT(h3_datagram_visitor.received_h3_datagrams(),
@@ -3180,23 +3240,41 @@
   session_->UnregisterHttp3DatagramFlowId(stream_->id());
 }
 
-TEST_P(QuicSpdyStreamTest, H3DatagramRegistrationWithContext) {
+TEST_P(QuicSpdyStreamTest, H3DatagramRegistrationWithoutContextDraft04) {
   if (!UsesHttp3()) {
     return;
   }
-  Initialize(kShouldProcessData);
-  session_->set_should_negotiate_h3_datagram(true);
-  QuicSpdySessionPeer::SetH3DatagramSupported(session_.get(), true);
-  session_->RegisterHttp3DatagramFlowId(stream_->id(), stream_->id());
+  InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
+  session_->set_local_http_datagram_support(HttpDatagramSupport::kDraft04);
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft04);
   ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor>
       h3_datagram_registration_visitor;
   SavingHttp3DatagramVisitor h3_datagram_visitor;
-  absl::optional<QuicDatagramContextId> context_id = 42;
-  Http3DatagramContextExtensions extensions;
+  absl::optional<QuicDatagramContextId> context_id;
+  absl::string_view format_additional_data;
+  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);
-  stream_->RegisterHttp3DatagramContextId(context_id, extensions,
-                                          &h3_datagram_visitor);
+
+  // Expect us to send a REGISTER_DATAGRAM_NO_CONTEXT capsule.
+  EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _))
+      .Times(AtLeast(1));
+
+  stream_->RegisterHttp3DatagramContextId(
+      context_id, DatagramFormatType::UDP_PAYLOAD, format_additional_data,
+      &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;
@@ -3204,9 +3282,75 @@
   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_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft04);
+  ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor>
+      h3_datagram_registration_visitor;
+  SavingHttp3DatagramVisitor h3_datagram_visitor;
+  absl::optional<QuicDatagramContextId> context_id = 42;
+  absl::string_view format_additional_data;
+  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(), _, _, _, _, _))
+      .Times(AtLeast(1));
+
+  stream_->RegisterHttp3DatagramContextId(
+      context_id, DatagramFormatType::UDP_PAYLOAD, format_additional_data,
+      &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(), _, _, _, _, _))
+      .Times(AtLeast(1));
+  stream_->UnregisterHttp3DatagramContextId(context_id);
+  stream_->UnregisterHttp3DatagramRegistrationVisitor();
   session_->UnregisterHttp3DatagramFlowId(stream_->id());
 }
 
@@ -3215,8 +3359,9 @@
     return;
   }
   Initialize(kShouldProcessData);
-  session_->set_should_negotiate_h3_datagram(true);
-  QuicSpdySessionPeer::SetH3DatagramSupported(session_.get(), true);
+  session_->set_local_http_datagram_support(HttpDatagramSupport::kDraft00And04);
+  QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+                                              HttpDatagramSupport::kDraft04);
   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/http/web_transport_http3.cc b/quic/core/http/web_transport_http3.cc
index b9b511c..9f5dd32 100644
--- a/quic/core/http/web_transport_http3.cc
+++ b/quic/core/http/web_transport_http3.cc
@@ -9,6 +9,7 @@
 
 #include "absl/strings/string_view.h"
 #include "absl/types/optional.h"
+#include "quic/core/http/capsule.h"
 #include "quic/core/http/quic_spdy_session.h"
 #include "quic/core/http/quic_spdy_stream.h"
 #include "quic/core/quic_data_reader.h"
@@ -201,13 +202,27 @@
 
 void WebTransportHttp3::OnContextReceived(
     QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
-    const Http3DatagramContextExtensions& /*extensions*/) {
+    DatagramFormatType format_type, absl::string_view format_additional_data) {
   if (stream_id != connect_stream_->id()) {
     QUIC_BUG(WT3 bad datagram context registration)
         << ENDPOINT << "Registered stream ID " << stream_id << ", expected "
         << connect_stream_->id();
     return;
   }
+  if (format_type != DatagramFormatType::WEBTRANSPORT) {
+    QUIC_DLOG(INFO) << ENDPOINT << "Ignoring unexpected datagram format type "
+                    << DatagramFormatTypeToString(format_type);
+    return;
+  }
+  if (!format_additional_data.empty()) {
+    QUIC_DLOG(ERROR)
+        << ENDPOINT
+        << "Received non-empty format additional data for context ID "
+        << (context_id_.has_value() ? context_id_.value() : 0)
+        << " on stream ID " << connect_stream_->id();
+    session_->ResetStream(connect_stream_->id(), QUIC_BAD_APPLICATION_PAYLOAD);
+    return;
+  }
   if (!context_is_known_) {
     context_is_known_ = true;
     context_id_ = context_id;
@@ -229,15 +244,14 @@
       return;
     }
     context_currently_registered_ = true;
-    Http3DatagramContextExtensions reply_extensions;
-    connect_stream_->RegisterHttp3DatagramContextId(context_id_,
-                                                    reply_extensions, this);
+    connect_stream_->RegisterHttp3DatagramContextId(
+        context_id_, format_type, format_additional_data, this);
   }
 }
 
 void WebTransportHttp3::OnContextClosed(
     QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
-    const Http3DatagramContextExtensions& /*extensions*/) {
+    ContextCloseCode close_code, absl::string_view close_details) {
   if (stream_id != connect_stream_->id()) {
     QUIC_BUG(WT3 bad datagram context registration)
         << ENDPOINT << "Closed context on stream ID " << stream_id
@@ -252,9 +266,12 @@
                     << " on stream ID " << connect_stream_->id();
     return;
   }
-  QUIC_DLOG(INFO) << ENDPOINT << "Received datagram context close on stream ID "
-                  << connect_stream_->id() << ", resetting stream";
-  session_->ResetStream(connect_stream_->id(), QUIC_STREAM_CANCELLED);
+  QUIC_DLOG(INFO) << ENDPOINT
+                  << "Received datagram context close with close code "
+                  << close_code << " close details \"" << close_details
+                  << "\" on stream ID " << connect_stream_->id()
+                  << ", resetting stream";
+  session_->ResetStream(connect_stream_->id(), QUIC_BAD_APPLICATION_PAYLOAD);
 }
 
 WebTransportHttp3UnidirectionalStream::WebTransportHttp3UnidirectionalStream(
diff --git a/quic/core/http/web_transport_http3.h b/quic/core/http/web_transport_http3.h
index ea1897c..f2fdb5a 100644
--- a/quic/core/http/web_transport_http3.h
+++ b/quic/core/http/web_transport_http3.h
@@ -76,12 +76,14 @@
                        absl::string_view payload) override;
 
   // From QuicSpdyStream::Http3DatagramRegistrationVisitor.
-  void OnContextReceived(
-      QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
-      const Http3DatagramContextExtensions& extensions) override;
-  void OnContextClosed(
-      QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
-      const Http3DatagramContextExtensions& extensions) override;
+  void OnContextReceived(QuicStreamId stream_id,
+                         absl::optional<QuicDatagramContextId> context_id,
+                         DatagramFormatType format_type,
+                         absl::string_view format_additional_data) override;
+  void OnContextClosed(QuicStreamId stream_id,
+                       absl::optional<QuicDatagramContextId> context_id,
+                       ContextCloseCode close_code,
+                       absl::string_view close_details) override;
 
  private:
   QuicSpdySession* const session_;        // Unowned.
diff --git a/quic/core/quic_constants.h b/quic/core/quic_constants.h
index 81c60e1..c53459c 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..1926968 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"
@@ -11,20 +13,13 @@
 namespace quic {
 
 MasqueClientSession::MasqueClientSession(
-    MasqueMode masque_mode,
-    const QuicConfig& config,
+    MasqueMode masque_mode, const QuicConfig& config,
     const ParsedQuicVersionVector& supported_versions,
-    QuicConnection* connection,
-    const QuicServerId& server_id,
+    QuicConnection* connection, const QuicServerId& server_id,
     QuicCryptoClientConfig* crypto_config,
-    QuicClientPushPromiseIndex* push_promise_index,
-    Owner* owner)
-    : QuicSpdyClientSession(config,
-                            supported_versions,
-                            connection,
-                            server_id,
-                            crypto_config,
-                            push_promise_index),
+    QuicClientPushPromiseIndex* push_promise_index, Owner* owner)
+    : QuicSpdyClientSession(config, supported_versions, connection, server_id,
+                            crypto_config, push_promise_index),
       masque_mode_(masque_mode),
       owner_(owner),
       compression_engine_(this) {}
@@ -101,7 +96,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) {
@@ -118,8 +115,7 @@
 
 void MasqueClientSession::SendPacket(
     QuicConnectionId client_connection_id,
-    QuicConnectionId server_connection_id,
-    absl::string_view packet,
+    QuicConnectionId server_connection_id, absl::string_view packet,
     const QuicSocketAddress& target_server_address,
     EncapsulatedClientSession* encapsulated_client_session) {
   if (masque_mode_ == MasqueMode::kLegacy) {
@@ -142,8 +138,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 +178,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()) {
@@ -196,8 +192,7 @@
 }
 
 void MasqueClientSession::OnConnectionClosed(
-    const QuicConnectionCloseFrame& frame,
-    ConnectionCloseSource source) {
+    const QuicConnectionCloseFrame& frame, ConnectionCloseSource source) {
   QuicSpdyClientSession::OnConnectionClosed(frame, source);
   // Close all encapsulated sessions.
   for (const auto& client_state : connect_udp_client_states_) {
@@ -224,8 +219,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 +236,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 +245,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;
 }
@@ -266,9 +263,9 @@
       target_server_address_(target_server_address) {
   QUICHE_DCHECK_NE(masque_session_, nullptr);
   this->stream()->RegisterHttp3DatagramRegistrationVisitor(this);
-  Http3DatagramContextExtensions extensions;
-  this->stream()->RegisterHttp3DatagramContextId(this->context_id(), extensions,
-                                                 this);
+  this->stream()->RegisterHttp3DatagramContextId(
+      this->context_id(), DatagramFormatType::UDP_PAYLOAD,
+      /*format_additional_data=*/absl::string_view(), this);
 }
 
 MasqueClientSession::ConnectUdpClientState::~ConnectUdpClientState() {
@@ -308,24 +305,40 @@
   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(
     QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
-    const Http3DatagramContextExtensions& /*extensions*/) {
+    DatagramFormatType format_type, absl::string_view format_additional_data) {
   if (stream_id != stream_->id()) {
     QUIC_BUG(MASQUE client bad datagram context registration)
         << "Registered stream ID " << stream_id << ", expected "
         << stream_->id();
     return;
   }
+  if (format_type != DatagramFormatType::UDP_PAYLOAD) {
+    QUIC_DLOG(INFO) << "Ignoring unexpected datagram format type "
+                    << DatagramFormatTypeToString(format_type);
+    return;
+  }
+  if (!format_additional_data.empty()) {
+    QUIC_DLOG(ERROR)
+        << "Received non-empty format additional data for context ID "
+        << (context_id_.has_value() ? context_id_.value() : 0)
+        << " on stream ID " << stream()->id();
+    masque_session_->ResetStream(stream()->id(), QUIC_STREAM_CANCELLED);
+    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
@@ -334,7 +347,7 @@
 
 void MasqueClientSession::ConnectUdpClientState::OnContextClosed(
     QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
-    const Http3DatagramContextExtensions& /*extensions*/) {
+    ContextCloseCode close_code, absl::string_view close_details) {
   if (stream_id != stream_->id()) {
     QUIC_BUG(MASQUE client bad datagram context registration)
         << "Closed context on stream ID " << stream_id << ", expected "
@@ -342,15 +355,18 @@
     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 "
-                  << stream_->id() << ", closing stream";
+  QUIC_DLOG(INFO) << "Received datagram context close with close code "
+                  << close_code << " close details \"" << close_details
+                  << "\" on stream ID " << stream_->id() << ", closing stream";
   masque_session_->ResetStream(stream_->id(), QUIC_STREAM_CANCELLED);
 }
 
diff --git a/quic/masque/masque_client_session.h b/quic/masque/masque_client_session.h
index 1c13087..662783a 100644
--- a/quic/masque/masque_client_session.h
+++ b/quic/masque/masque_client_session.h
@@ -144,14 +144,14 @@
                          absl::string_view payload) override;
 
     // From QuicSpdyStream::Http3DatagramRegistrationVisitor.
-    void OnContextReceived(
-        QuicStreamId stream_id,
-        absl::optional<QuicDatagramContextId> context_id,
-        const Http3DatagramContextExtensions& extensions) override;
-    void OnContextClosed(
-        QuicStreamId stream_id,
-        absl::optional<QuicDatagramContextId> context_id,
-        const Http3DatagramContextExtensions& extensions) override;
+    void OnContextReceived(QuicStreamId stream_id,
+                           absl::optional<QuicDatagramContextId> context_id,
+                           DatagramFormatType format_type,
+                           absl::string_view format_additional_data) override;
+    void OnContextClosed(QuicStreamId stream_id,
+                         absl::optional<QuicDatagramContextId> context_id,
+                         ContextCloseCode close_code,
+                         absl::string_view close_details) override;
 
    private:
     QuicSpdyClientStream* stream_;                            // Unowned.
@@ -161,7 +161,9 @@
     QuicSocketAddress target_server_address_;
   };
 
-  bool ShouldNegotiateHttp3Datagram() override { return true; }
+  HttpDatagramSupport LocalHttpDatagramSupport() override {
+    return HttpDatagramSupport::kDraft00And04;
+  }
 
   const ConnectUdpClientState* GetOrCreateConnectUdpClientState(
       const QuicSocketAddress& target_server_address,
diff --git a/quic/masque/masque_epoll_client.cc b/quic/masque/masque_epoll_client.cc
index ec47520..2b835cf 100644
--- a/quic/masque/masque_epoll_client.cc
+++ b/quic/masque/masque_epoll_client.cc
@@ -68,7 +68,7 @@
     return nullptr;
   }
 
-  masque_client->set_initial_max_packet_length(kDefaultMaxPacketSize);
+  masque_client->set_initial_max_packet_length(kMasqueMaxOuterPacketSize);
   masque_client->set_drop_response_body(false);
   if (!masque_client->Initialize()) {
     QUIC_LOG(ERROR) << "Failed to initialize masque_client";
diff --git a/quic/masque/masque_server_session.cc b/quic/masque/masque_server_session.cc
index 575ac9f..693f009 100644
--- a/quic/masque/masque_server_session.cc
+++ b/quic/masque/masque_server_session.cc
@@ -201,13 +201,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() != "/") {
@@ -234,7 +236,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
                     << "\"";
 
@@ -263,20 +267,27 @@
           << 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.
+      stream->RegisterHttp3DatagramContextId(
+          context_id, DatagramFormatType::UDP_PAYLOAD,
+          /*format_additional_data=*/absl::string_view(),
+          &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));
@@ -423,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,
@@ -439,10 +463,10 @@
 
 MasqueServerSession::ConnectUdpServerState::~ConnectUdpServerState() {
   if (stream() != nullptr) {
-    stream()->UnregisterHttp3DatagramRegistrationVisitor();
     if (context_registered_) {
       stream()->UnregisterHttp3DatagramContextId(context_id());
     }
+    stream()->UnregisterHttp3DatagramRegistrationVisitor();
   }
   if (fd_ == kQuicInvalidSocketFd) {
     return;
@@ -502,39 +526,55 @@
 
 void MasqueServerSession::ConnectUdpServerState::OnContextReceived(
     QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
-    const Http3DatagramContextExtensions& /*extensions*/) {
+    DatagramFormatType format_type, absl::string_view format_additional_data) {
   if (stream_id != stream()->id()) {
     QUIC_BUG(MASQUE server bad datagram context registration)
         << "Registered stream ID " << stream_id << ", expected "
         << stream()->id();
     return;
   }
+  if (format_type != DatagramFormatType::UDP_PAYLOAD) {
+    QUIC_DLOG(INFO) << "Ignoring unexpected datagram format type "
+                    << DatagramFormatTypeToString(format_type);
+    return;
+  }
+  if (!format_additional_data.empty()) {
+    QUIC_DLOG(ERROR)
+        << "Received non-empty format additional data for context ID "
+        << (context_id_.has_value() ? context_id_.value() : 0)
+        << " on stream ID " << stream()->id();
+    masque_session_->ResetStream(stream()->id(), QUIC_STREAM_CANCELLED);
+    return;
+  }
   if (!context_received_) {
     context_received_ = true;
     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;
-  Http3DatagramContextExtensions reply_extensions;
-  stream()->RegisterHttp3DatagramContextId(context_id_, reply_extensions, this);
+  stream()->RegisterHttp3DatagramContextId(context_id_, format_type,
+                                           format_additional_data, this);
 }
 
 void MasqueServerSession::ConnectUdpServerState::OnContextClosed(
     QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
-    const Http3DatagramContextExtensions& /*extensions*/) {
+    ContextCloseCode close_code, absl::string_view close_details) {
   if (stream_id != stream()->id()) {
     QUIC_BUG(MASQUE server bad datagram context registration)
         << "Closed context on stream ID " << stream_id << ", expected "
@@ -542,15 +582,18 @@
     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 "
-                  << stream()->id() << ", closing stream";
+  QUIC_DLOG(INFO) << "Received datagram context close with close code "
+                  << close_code << " close details \"" << close_details
+                  << "\" on stream ID " << stream()->id() << ", closing stream";
   masque_session_->ResetStream(stream()->id(), QUIC_STREAM_CANCELLED);
 }
 
diff --git a/quic/masque/masque_server_session.h b/quic/masque/masque_server_session.h
index 1bec14c..b3c327f 100644
--- a/quic/masque/masque_server_session.h
+++ b/quic/masque/masque_server_session.h
@@ -117,14 +117,14 @@
                          absl::string_view payload) override;
 
     // From QuicSpdyStream::Http3DatagramRegistrationVisitor.
-    void OnContextReceived(
-        QuicStreamId stream_id,
-        absl::optional<QuicDatagramContextId> context_id,
-        const Http3DatagramContextExtensions& extensions) override;
-    void OnContextClosed(
-        QuicStreamId stream_id,
-        absl::optional<QuicDatagramContextId> context_id,
-        const Http3DatagramContextExtensions& extensions) override;
+    void OnContextReceived(QuicStreamId stream_id,
+                           absl::optional<QuicDatagramContextId> context_id,
+                           DatagramFormatType format_type,
+                           absl::string_view format_additional_data) override;
+    void OnContextClosed(QuicStreamId stream_id,
+                         absl::optional<QuicDatagramContextId> context_id,
+                         ContextCloseCode close_code,
+                         absl::string_view close_details) override;
 
    private:
     QuicSpdyStream* stream_;
@@ -136,7 +136,11 @@
     bool context_registered_ = false;
   };
 
-  bool ShouldNegotiateHttp3Datagram() override { return true; }
+  // From QuicSpdySession.
+  bool OnSettingsFrame(const SettingsFrame& frame) override;
+  HttpDatagramSupport LocalHttpDatagramSupport() override {
+    return HttpDatagramSupport::kDraft00And04;
+  }
 
   MasqueServerBackend* masque_server_backend_;  // Unowned.
   Visitor* owner_;                              // Unowned.
diff --git a/quic/masque/masque_utils.h b/quic/masque/masque_utils.h
index 8113047..abaeda7 100644
--- a/quic/masque/masque_utils.h
+++ b/quic/masque/masque_utils.h
@@ -18,7 +18,10 @@
 QUIC_NO_EXPORT QuicConfig MasqueEncapsulatedConfig();
 
 // Maximum packet size for encapsulated connections.
-enum : QuicByteCount { kMasqueMaxEncapsulatedPacketSize = 1300 };
+enum : QuicByteCount {
+  kMasqueMaxEncapsulatedPacketSize = 1300,
+  kMasqueMaxOuterPacketSize = 1350,
+};
 
 // Mode that MASQUE is operating in.
 enum class MasqueMode : uint8_t {
diff --git a/quic/test_tools/quic_spdy_session_peer.cc b/quic/test_tools/quic_spdy_session_peer.cc
index a27f073..f0f99ee 100644
--- a/quic/test_tools/quic_spdy_session_peer.cc
+++ b/quic/test_tools/quic_spdy_session_peer.cc
@@ -40,18 +40,14 @@
 }
 
 void QuicSpdySessionPeer::SetMaxInboundHeaderListSize(
-    QuicSpdySession* session,
-    size_t max_inbound_header_size) {
+    QuicSpdySession* session, size_t max_inbound_header_size) {
   session->set_max_inbound_header_list_size(max_inbound_header_size);
 }
 
 // static
 size_t QuicSpdySessionPeer::WriteHeadersOnHeadersStream(
-    QuicSpdySession* session,
-    QuicStreamId id,
-    spdy::SpdyHeaderBlock headers,
-    bool fin,
-    const spdy::SpdyStreamPrecedence& precedence,
+    QuicSpdySession* session, QuicStreamId id, spdy::SpdyHeaderBlock headers,
+    bool fin, const spdy::SpdyStreamPrecedence& precedence,
     QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener) {
   return session->WriteHeadersOnHeadersStream(
       id, std::move(headers), fin, precedence, std::move(ack_listener));
@@ -100,22 +96,22 @@
 }
 
 // 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
-bool QuicSpdySessionPeer::ShouldNegotiateHttp3Datagram(
+HttpDatagramSupport QuicSpdySessionPeer::LocalHttpDatagramSupport(
     QuicSpdySession* session) {
-  return session->ShouldNegotiateHttp3Datagram();
+  return session->LocalHttpDatagramSupport();
 }
 
 // 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::kDraft04);
+  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..ba28e67 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 {
 
@@ -32,11 +32,8 @@
   static void SetMaxInboundHeaderListSize(QuicSpdySession* session,
                                           size_t max_inbound_header_size);
   static size_t WriteHeadersOnHeadersStream(
-      QuicSpdySession* session,
-      QuicStreamId id,
-      spdy::SpdyHeaderBlock headers,
-      bool fin,
-      const spdy::SpdyStreamPrecedence& precedence,
+      QuicSpdySession* session, QuicStreamId id, spdy::SpdyHeaderBlock headers,
+      bool fin, const spdy::SpdyStreamPrecedence& precedence,
       QuicReferenceCountedPointer<QuicAckListenerInterface> ack_listener);
   // |session| can't be nullptr.
   static QuicStreamId GetNextOutgoingUnidirectionalStreamId(
@@ -50,10 +47,10 @@
       QuicSpdySession* session);
   static QpackReceiveStream* GetQpackEncoderReceiveStream(
       QuicSpdySession* session);
-  static void SetH3DatagramSupported(QuicSpdySession* session,
-                                     bool h3_datagram_supported);
-  static bool ShouldNegotiateHttp3Datagram(QuicSpdySession* session);
-  static void EnableWebTransport(QuicSpdySession& session);
+  static void SetHttpDatagramSupport(QuicSpdySession* session,
+                                     HttpDatagramSupport http_datagram_support);
+  static HttpDatagramSupport LocalHttpDatagramSupport(QuicSpdySession* session);
+  static void EnableWebTransport(QuicSpdySession* session);
 };
 
 }  // namespace test
diff --git a/quic/test_tools/quic_test_utils.h b/quic/test_tools/quic_test_utils.h
index 0831b81..7053829 100644
--- a/quic/test_tools/quic_test_utils.h
+++ b/quic/test_tools/quic_test_utils.h
@@ -1995,13 +1995,14 @@
   MOCK_METHOD(void, OnContextReceived,
               (QuicStreamId stream_id,
                absl::optional<QuicDatagramContextId> context_id,
-               const Http3DatagramContextExtensions& extensions),
+               DatagramFormatType format_type,
+               absl::string_view format_additional_data),
               (override));
 
   MOCK_METHOD(void, OnContextClosed,
               (QuicStreamId stream_id,
                absl::optional<QuicDatagramContextId> context_id,
-               const Http3DatagramContextExtensions& extensions),
+               ContextCloseCode close_code, absl::string_view close_details),
               (override));
 };
 
diff --git a/quic/tools/quic_simple_client_session.cc b/quic/tools/quic_simple_client_session.cc
index 07c4628..47c521b 100644
--- a/quic/tools/quic_simple_client_session.cc
+++ b/quic/tools/quic_simple_client_session.cc
@@ -54,8 +54,9 @@
   return enable_web_transport_;
 }
 
-bool QuicSimpleClientSession::ShouldNegotiateHttp3Datagram() {
-  return enable_web_transport_;
+HttpDatagramSupport QuicSimpleClientSession::LocalHttpDatagramSupport() {
+  return enable_web_transport_ ? HttpDatagramSupport::kDraft04
+                               : HttpDatagramSupport::kNone;
 }
 
 }  // namespace quic
diff --git a/quic/tools/quic_simple_client_session.h b/quic/tools/quic_simple_client_session.h
index 124e56f..1a6e694 100644
--- a/quic/tools/quic_simple_client_session.h
+++ b/quic/tools/quic_simple_client_session.h
@@ -30,7 +30,7 @@
 
   std::unique_ptr<QuicSpdyClientStream> CreateClientStream() override;
   bool ShouldNegotiateWebTransport() override;
-  bool ShouldNegotiateHttp3Datagram() override;
+  HttpDatagramSupport LocalHttpDatagramSupport() override;
 
  private:
   const bool drop_response_body_;
diff --git a/quic/tools/quic_simple_server_session.h b/quic/tools/quic_simple_server_session.h
index 2cc3dcf..9746f26 100644
--- a/quic/tools/quic_simple_server_session.h
+++ b/quic/tools/quic_simple_server_session.h
@@ -96,9 +96,11 @@
   bool ShouldNegotiateWebTransport() override {
     return quic_simple_server_backend_->SupportsWebTransport();
   }
-  bool ShouldNegotiateHttp3Datagram() override {
-    return QuicServerSessionBase::ShouldNegotiateHttp3Datagram() ||
-           ShouldNegotiateWebTransport();
+  HttpDatagramSupport LocalHttpDatagramSupport() override {
+    if (ShouldNegotiateWebTransport()) {
+      return HttpDatagramSupport::kDraft00And04;
+    }
+    return QuicServerSessionBase::LocalHttpDatagramSupport();
   }
 
  private: