@ 261106.9
current
#include "guard.h"
#ifndef OPENSSL_NO_QUIC
#include <async_wrap-inl.h>
#include <base_object-inl.h>
#include <debug_utils-inl.h>
#include <env-inl.h>
#include <memory_tracker-inl.h>
#include <nghttp3/nghttp3.h>
#include <ngtcp2/ngtcp2.h>
#include <node_http_common-inl.h>
#include <node_sockaddr-inl.h>
#include <util-inl.h>
#include <zlib.h>
#include "application.h"
#include "bindingdata.h"
#include "defs.h"
#include "http3.h"
#include "session.h"
#include "sessionticket.h"

namespace node {

using v8::Array;
using v8::Global;
using v8::Integer;
using v8::Local;
using v8::LocalVector;
using v8::Value;

namespace quic {

namespace {
constexpr uint8_t kSessionTicketAppDataVersion = 1;
// Layout: [type(1)][version(1)][crc(4)][payload(34)] = 40 bytes
constexpr size_t kSessionTicketAppDataSize = 40;
constexpr size_t kSessionTicketAppDataHeaderSize = 6;  // type + version + crc
constexpr size_t kSessionTicketAppDataPayloadSize =
    kSessionTicketAppDataSize - kSessionTicketAppDataHeaderSize;

inline void WriteBE32(uint8_t* buf, uint32_t val) {
  buf[0] = static_cast<uint8_t>((val >> 24) & 0xff);
  buf[1] = static_cast<uint8_t>((val >> 16) & 0xff);
  buf[2] = static_cast<uint8_t>((val >> 8) & 0xff);
  buf[3] = static_cast<uint8_t>(val & 0xff);
}

inline uint32_t ReadBE32(const uint8_t* buf) {
  return (static_cast<uint32_t>(buf[0]) << 24) |
         (static_cast<uint32_t>(buf[1]) << 16) |
         (static_cast<uint32_t>(buf[2]) << 8) | static_cast<uint32_t>(buf[3]);
}

inline void WriteBE64(uint8_t* buf, uint64_t val) {
  buf[0] = static_cast<uint8_t>((val >> 56) & 0xff);
  buf[1] = static_cast<uint8_t>((val >> 48) & 0xff);
  buf[2] = static_cast<uint8_t>((val >> 40) & 0xff);
  buf[3] = static_cast<uint8_t>((val >> 32) & 0xff);
  buf[4] = static_cast<uint8_t>((val >> 24) & 0xff);
  buf[5] = static_cast<uint8_t>((val >> 16) & 0xff);
  buf[6] = static_cast<uint8_t>((val >> 8) & 0xff);
  buf[7] = static_cast<uint8_t>(val & 0xff);
}

inline uint64_t ReadBE64(const uint8_t* buf) {
  return (static_cast<uint64_t>(buf[0]) << 56) |
         (static_cast<uint64_t>(buf[1]) << 48) |
         (static_cast<uint64_t>(buf[2]) << 40) |
         (static_cast<uint64_t>(buf[3]) << 32) |
         (static_cast<uint64_t>(buf[4]) << 24) |
         (static_cast<uint64_t>(buf[5]) << 16) |
         (static_cast<uint64_t>(buf[6]) << 8) | static_cast<uint64_t>(buf[7]);
}

// Serialize an nghttp3_pri into an RFC 9218 priority field value
// (e.g., "u=3" or "u=0, i"). Returns the number of bytes written.
// This is used only for setting the priority field of HTTP/3 streams on
// the client side.
inline size_t FormatPriority(char* buf, size_t buflen, const nghttp3_pri& pri) {
  int len;
  if (pri.inc) {
    len = snprintf(buf, buflen, "u=%d, i", pri.urgency);
  } else {
    len = snprintf(buf, buflen, "u=%d", pri.urgency);
  }
  return static_cast<size_t>(len);
}
}  // namespace

struct Http3HeadersTraits {
  using nv_t = nghttp3_nv;
};

struct Http3RcBufferPointerTraits {
  using rcbuf_t = nghttp3_rcbuf;
  using vector_t = nghttp3_vec;

  static void inc(rcbuf_t* buf) {
    CHECK_NOT_NULL(buf);
    nghttp3_rcbuf_incref(buf);
  }
  static void dec(rcbuf_t* buf) {
    CHECK_NOT_NULL(buf);
    nghttp3_rcbuf_decref(buf);
  }
  static vector_t get_vec(rcbuf_t* buf) {
    CHECK_NOT_NULL(buf);
    return nghttp3_rcbuf_get_buf(buf);
  }
  static bool is_static(const rcbuf_t* buf) {
    CHECK_NOT_NULL(buf);
    return nghttp3_rcbuf_is_static(buf);
  }
};

using Http3ConnectionPointer = DeleteFnPtr<nghttp3_conn, nghttp3_conn_del>;
using Http3Headers = NgHeaders<Http3HeadersTraits>;
using Http3RcBufferPointer = NgRcBufPointer<Http3RcBufferPointerTraits>;

struct Http3HeaderTraits {
  typedef Http3RcBufferPointer rcbufferpointer_t;
  typedef BindingData allocator_t;

  static const char* ToHttpHeaderName(int32_t token) {
    switch (token) {
      case -1:
        return nullptr;
#define V(key, name)                                                           \
  case NGHTTP3_QPACK_TOKEN__##key:                                             \
    return name;
        HTTP_SPECIAL_HEADERS(V)
#undef V
#define V(key, name)                                                           \
  case NGHTTP3_QPACK_TOKEN_##key:                                              \
    return name;
        HTTP_REGULAR_HEADERS(V)
#undef V
    }
    return nullptr;
  }
};

using Http3Header = NgHeader<Http3HeaderTraits>;

struct Http3StreamState final : public StreamApplicationState {
  struct PendingHeaders final {
    HeadersKind kind;
    Global<Array> headers;
    HeadersFlags flags;

    PendingHeaders(HeadersKind kind, Global<Array> headers, HeadersFlags flags)
        : kind(kind), headers(std::move(headers)), flags(flags) {}
    DISALLOW_COPY_AND_MOVE(PendingHeaders)
  };

  std::vector<std::unique_ptr<PendingHeaders>> pending_headers;
  std::vector<std::unique_ptr<Http3Header>> headers;
  HeadersKind headers_kind = HeadersKind::INITIAL;
  size_t headers_length = 0;
  bool wants_headers = false;
  bool wants_trailers = false;
};

// Implements the low-level HTTP/3 Application semantics.
class Http3ApplicationImpl final : public Session::Application {
 public:
  Http3ApplicationImpl(Session* session, const Options& options)
      : Application(session, options),
        allocator_(BindingData::Get(env()).nghttp3_allocator()),
        options_(options),
        conn_(nullptr) {
    // Build the ORIGIN frame payload from the SNI configuration before
    // creating the nghttp3 connection, since InitializeConnection needs
    // the origin_vec_ to be ready for settings.origin_list.
    if (session->is_server()) {
      BuildOriginPayload();
    }
    conn_ = InitializeConnection();
    session->set_priority_supported();
  }

  const Options& options() const override { return options_; }

  Session::Application::Type type() const override {
    return Session::Application::Type::HTTP3;
  }

  error_code GetNoErrorCode() const override { return NGHTTP3_H3_NO_ERROR; }

  // HTTP/3 defines H3_INTERNAL_ERROR (0x102) for non-specific failures
  // initiated by the implementation; this is the right code to send
  // on RESET_STREAM when a stream is being aborted without an
  // application-supplied code.
  error_code GetInternalErrorCode() const override {
    return NGHTTP3_H3_INTERNAL_ERROR;
  }

  error_code GetRequestRejectedCode() const override {
    return NGHTTP3_H3_REQUEST_REJECTED;
  }

  void EarlyDataRejected() override {
    // When 0-RTT is rejected, destroy the nghttp3 connection and all
    // open streams — ngtcp2 has discarded their internal state.
    // Reset started_ so Start() is called again via on_receive_rx_key
    // at 1RTT to recreate the nghttp3 connection. Use the
    // application's internal error code since this is an error
    // condition (code 0 would be treated as a clean close).
    conn_.reset();
    started_ = false;
    session().DestroyAllStreams(
        QuicError::ForApplication(GetInternalErrorCode()));
    if (!session().is_destroyed()) {
      session().EmitEarlyDataRejected();
    }
  }

  bool ReceiveStreamOpen(stream_id id) override {
    // In HTTP/3, only create Stream objects for bidirectional streams.
    // Unidirectional streams (control, QPACK encoder/decoder) are
    // managed internally by nghttp3 and should not be exposed to JS.
    if (!ngtcp2_is_bidi_stream(id)) return true;
    auto stream = session().CreateStream(id);
    if (!stream || session().is_destroyed()) [[unlikely]] {
      return !session().is_destroyed();
    }
    return true;
  }

  bool SupportsHeaders() const override { return true; }

  bool SupportsStreamCallbacks() const override { return true; }

  bool is_started() const override { return started_; }

  bool Start() override {
    if (started_) return true;
    Debug(&session(), "Starting HTTP/3 application.");

    const auto params = session().remote_transport_params();
    if (!params) [[unlikely]] {
      // The params are not available yet. Cannot start.
      Debug(&session(),
            "Cannot start HTTP/3 application yet. No remote transport params");
      return false;
    }

    if (params.initial_max_streams_uni() < 3) {
      // HTTP3 requires 3 unidirectional control streams to be opened in each
      // direction in additional to the bidirectional streams that are used to
      // actually carry request and response payload back and forth.
      // See:
      // https://nghttp2.org/nghttp3/programmers-guide.html#binding-control-streams
      Debug(&session(),
            "Cannot start HTTP/3 application. Initial max "
            "unidirectional streams [%" PRIu64
            "] is too low. Must be at least 3",
            params.initial_max_streams_uni());
      return false;
    }

    // If this is a server session, then set the maximum number of
    // bidirectional streams that can be created. This determines the number
    // of requests that the client can actually created.
    if (session().is_server()) {
      nghttp3_conn_set_max_client_streams_bidi(
          *this, params.initial_max_streams_bidi());
    }

    Debug(&session(), "Creating and binding HTTP/3 control streams");
    started_ =
        session().OpenUnidirectionalStream(&control_stream_id_) &&
        session().OpenUnidirectionalStream(&qpack_enc_stream_id_) &&
        session().OpenUnidirectionalStream(&qpack_dec_stream_id_) &&
        nghttp3_conn_bind_control_stream(*this, control_stream_id_) == 0 &&
        nghttp3_conn_bind_qpack_streams(
            *this, qpack_enc_stream_id_, qpack_dec_stream_id_) == 0;

    if (env()->enabled_debug_list()->enabled(DebugCategory::QUIC) && started_) {
      Debug(&session(),
            "Created and bound control stream %" PRIi64,
            control_stream_id_);
      Debug(&session(),
            "Created and bound qpack enc stream %" PRIi64,
            qpack_enc_stream_id_);
      Debug(&session(),
            "Created and bound qpack dec streams %" PRIi64,
            qpack_dec_stream_id_);
    }

    return started_;
  }

  void BeginShutdown() override {
    // Only submit a shutdown notice if the H3 connection was fully
    // started (control streams bound). If the TLS handshake failed
    // before Start() was called, conn_ exists but its control streams
    // are unbound, and nghttp3_conn_submit_shutdown_notice would crash.
    if (conn_ && started_) nghttp3_conn_submit_shutdown_notice(*this);
  }

  void CompleteShutdown() override {
    // Same guard as BeginShutdown — nghttp3_conn_shutdown asserts
    // that the control stream is bound (conn->tx.ctrl != NULL).
    if (conn_ && started_) nghttp3_conn_shutdown(*this);
  }

  bool ReceiveStreamData(stream_id id,
                         const uint8_t* data,
                         size_t datalen,
                         const Stream::ReceiveDataFlags& flags,
                         void* unused) override {
    Debug(&session(),
          "HTTP/3 application received %zu bytes of data "
          "on stream %" PRIi64 ". Is final? %d. Is early? %d",
          datalen,
          id,
          flags.fin,
          flags.early);

    auto nread = nghttp3_conn_read_stream2(
        *this, id, data, datalen, flags.fin ? 1 : 0, uv_hrtime());

    if (nread < 0) {
      Debug(&session(),
            "HTTP/3 application failed to read stream data: %s",
            nghttp3_strerror(nread));
      return false;
    }

    if (nread > 0) {
      Debug(&session(),
            "Extending stream and connection offset by %zd bytes",
            nread);
      session().Consume(id, nread);
    }

    // If this data arrived as 0-RTT, mark the stream. We set it after
    // nghttp3_conn_read_stream2 because the stream may not exist until
    // nghttp3 processes the headers (via on_begin_headers).
    if (flags.early) {
      if (auto stream = session().FindStream(id)) {
        stream->set_early();
      }
    }

    return true;
  }

  bool AcknowledgeStreamData(stream_id id, size_t datalen) override {
    Debug(&session(),
          "HTTP/3 application received acknowledgement for %zu bytes of data "
          "on stream %" PRIi64,
          datalen,
          id);
    return nghttp3_conn_add_ack_offset(*this, id, datalen) == 0;
  }

  bool stream_fin_managed_by_application() const override { return true; }

  void StreamWriteShut(stream_id id) override {
    nghttp3_conn_shutdown_stream_write(*this, id);
  }

  void BlockStream(stream_id id) override {
    nghttp3_conn_block_stream(*this, id);
    Application::BlockStream(id);
  }

  void ResumeStream(stream_id id) override {
    nghttp3_conn_resume_stream(*this, id);
    Application::ResumeStream(id);
  }

  void ExtendMaxStreamData(Stream* stream, uint64_t max_data) override {
    Debug(&session(),
          "HTTP/3 application extending max stream data to %" PRIu64,
          max_data);
    stream->UpdateWriteDesiredSize();  // the stream might be blocked on js side
    nghttp3_conn_unblock_stream(*this, stream->id());
  }

  void CollectSessionTicketAppData(
      SessionTicket::AppData* app_data) const override {
    uint8_t buf[kSessionTicketAppDataSize];
    buf[0] = static_cast<uint8_t>(Type::HTTP3);
    buf[1] = kSessionTicketAppDataVersion;

    uint8_t* payload = buf + kSessionTicketAppDataHeaderSize;
    WriteBE64(payload, options_.max_field_section_size);
    WriteBE64(payload + 8, options_.qpack_max_dtable_capacity);
    WriteBE64(payload + 16, options_.qpack_encoder_max_dtable_capacity);
    WriteBE64(payload + 24, options_.qpack_blocked_streams);
    payload[32] = options_.enable_connect_protocol ? 1 : 0;
    payload[33] = options_.enable_datagrams ? 1 : 0;

    uLong crc = crc32(0L, Z_NULL, 0);
    crc = crc32(crc, payload, kSessionTicketAppDataPayloadSize);
    WriteBE32(buf + 2, static_cast<uint32_t>(crc));

    app_data->Set(
        uv_buf_init(reinterpret_cast<char*>(buf), kSessionTicketAppDataSize));
  }

  SessionTicket::AppData::Status ExtractSessionTicketAppData(
      const SessionTicket::AppData& app_data,
      SessionTicket::AppData::Source::Flag flag) override {
    auto data = app_data.Get();
    if (!data || data->len != kSessionTicketAppDataSize) {
      return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW;
    }

    const uint8_t* buf = reinterpret_cast<const uint8_t*>(data->base);

    // buf[0] is the application type byte, buf[1] is the version.
    if (buf[0] != static_cast<uint8_t>(Type::HTTP3) ||
        buf[1] != kSessionTicketAppDataVersion) {
      Debug(&session(),
            "Ticket app data rejected: type=%d version=%d "
            "(expected type=%d version=%d)",
            buf[0],
            buf[1],
            static_cast<uint8_t>(Type::HTTP3),
            kSessionTicketAppDataVersion);
      return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW;
    }

    const uint8_t* payload = buf + kSessionTicketAppDataHeaderSize;
    uint32_t stored_crc = ReadBE32(buf + 2);
    uLong computed_crc = crc32(0L, Z_NULL, 0);
    computed_crc =
        crc32(computed_crc, payload, kSessionTicketAppDataPayloadSize);
    if (stored_crc != static_cast<uint32_t>(computed_crc)) {
      Debug(&session(),
            "Ticket app data rejected: CRC mismatch "
            "(stored=%u computed=%u)",
            stored_crc,
            static_cast<uint32_t>(computed_crc));
      return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW;
    }

    uint64_t stored_max_field_section_size = ReadBE64(payload);
    uint64_t stored_qpack_max_dtable_capacity = ReadBE64(payload + 8);
    uint64_t stored_qpack_encoder_max_dtable_capacity = ReadBE64(payload + 16);
    uint64_t stored_qpack_blocked_streams = ReadBE64(payload + 24);
    bool stored_enable_connect_protocol = payload[32] != 0;
    bool stored_enable_datagrams = payload[33] != 0;

    Debug(&session(),
          "Ticket app data: stored mfss=%" PRIu64 " qmdc=%" PRIu64
          " qemdc=%" PRIu64 " qbs=%" PRIu64 " ecp=%d ed=%d",
          stored_max_field_section_size,
          stored_qpack_max_dtable_capacity,
          stored_qpack_encoder_max_dtable_capacity,
          stored_qpack_blocked_streams,
          stored_enable_connect_protocol,
          stored_enable_datagrams);
    Debug(&session(),
          "Current opts: mfss=%" PRIu64 " qmdc=%" PRIu64 " qemdc=%" PRIu64
          " qbs=%" PRIu64 " ecp=%d ed=%d",
          options_.max_field_section_size,
          options_.qpack_max_dtable_capacity,
          options_.qpack_encoder_max_dtable_capacity,
          options_.qpack_blocked_streams,
          options_.enable_connect_protocol,
          options_.enable_datagrams);
    if (options_.max_field_section_size < stored_max_field_section_size ||
        options_.qpack_max_dtable_capacity < stored_qpack_max_dtable_capacity ||
        options_.qpack_encoder_max_dtable_capacity <
            stored_qpack_encoder_max_dtable_capacity ||
        options_.qpack_blocked_streams < stored_qpack_blocked_streams ||
        (stored_enable_connect_protocol && !options_.enable_connect_protocol) ||
        (stored_enable_datagrams && !options_.enable_datagrams)) {
      Debug(&session(), "Ticket app data REJECTED");
      return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW;
    }
    Debug(&session(), "Ticket app data ACCEPTED");

    return flag == SessionTicket::AppData::Source::Flag::STATUS_RENEW
               ? SessionTicket::AppData::Status::TICKET_USE_RENEW
               : SessionTicket::AppData::Status::TICKET_USE;
  }

  void ReceiveStreamClose(stream_id id,
                          Stream* stream,
                          QuicError&& error = QuicError()) override {
    Debug(&session(), "HTTP/3 application closing stream %" PRIi64, id);

    // Clean up nghttp3's state first. N.b. destroying the Stream calls into
    // JS, so this can tear down the session. Skip unidirectional streams
    // (control/QPACK) as nghttp3 handles this and would reject if we try.
    if (conn_ && ngtcp2_is_bidi_stream(id)) {
      int rv = nghttp3_conn_close_stream2(
          *this, NGHTTP3_STREAM_CLOSE_FLAG_NONE, id, 0, 0);
      if (rv != 0 && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) {
        session().SetApplicationError(
            nghttp3_err_infer_quic_app_error_code(rv));
        session().Close();
        return;
      }
    }

    Application::ReceiveStreamClose(id, stream, std::move(error));
  }

  void ReceiveStreamReset(Stream* stream,
                          uint64_t final_size,
                          QuicError&& error = QuicError()) override {
    // We are shutting down the readable side of the local stream here.
    Debug(&session(),
          "HTTP/3 application resetting stream %" PRIi64,
          stream->id());
    int rv = nghttp3_conn_shutdown_stream_read(*this, stream->id());
    if (rv == 0) {
      stream->ReceiveStreamReset(final_size, std::move(error));
      return;
    }

    session().SetApplicationError(nghttp3_err_infer_quic_app_error_code(rv));
    session().Close();
  }

  void ReceiveStreamStopSending(Stream* stream,
                                QuicError&& error = QuicError()) override {
    Application::ReceiveStreamStopSending(stream, std::move(error));
  }

  bool StreamOpened(Stream& stream) override {
    auto* state = GetStreamState(stream);
    if (state == nullptr || state->pending_headers.empty()) return true;

    decltype(state->pending_headers) pending;
    state->pending_headers.swap(pending);
    Session::SendPendingDataScope send_scope(&session());
    for (auto& headers : pending) {
      if (!SubmitHeaders(stream,
                         headers->kind,
                         headers->headers.Get(env()->isolate()),
                         headers->flags)) {
        return false;
      }
    }
    return true;
  }

  void StreamRemoved(stream_id id) override {
    if (conn_) nghttp3_conn_set_stream_user_data(*this, id, nullptr);
  }

  bool SendHeaders(Stream& stream,
                   HeadersKind kind,
                   const Local<Array>& headers,
                   HeadersFlags flags = HeadersFlags::NONE) override {
    if (kind == HeadersKind::HINTS && !session().is_server()) return false;

    if (stream.is_pending()) {
      Debug(&session(), "Enqueuing headers for pending HTTP/3 stream");
      auto& state = GetOrCreateStreamState(stream);
      state.pending_headers.push_back(
          std::make_unique<Http3StreamState::PendingHeaders>(
              kind, Global<Array>(env()->isolate(), headers), flags));
      return true;
    }

    Session::SendPendingDataScope send_scope(&session());
    return SubmitHeaders(stream, kind, headers, flags);
  }

  void SetHeadersInterest(Stream& stream,
                          bool wants_headers,
                          bool wants_trailers) override {
    auto& state = GetOrCreateStreamState(stream);
    state.wants_headers = wants_headers;
    state.wants_trailers = wants_trailers;
  }

  void SetStreamPriority(const Stream& stream,
                         StreamPriority priority,
                         StreamPriorityFlags flags) override {
    nghttp3_pri pri;
    pri.inc = (flags == StreamPriorityFlags::INCREMENTAL) ? 1 : 0;
    switch (priority) {
      case StreamPriority::HIGH:
        pri.urgency = NGHTTP3_URGENCY_HIGH;
        break;
      case StreamPriority::LOW:
        pri.urgency = NGHTTP3_URGENCY_LOW;
        break;
      default:
        pri.urgency = NGHTTP3_DEFAULT_URGENCY;
        break;
    }
    if (session().is_server()) {
      nghttp3_conn_set_server_stream_priority(*this, stream.id(), &pri);
    } else {
      // The client API takes a serialized RFC 9218 priority field value
      // (e.g., "u=0, i") rather than an nghttp3_pri struct.
      char buf[8];
      size_t len = FormatPriority(buf, sizeof(buf), pri);
      nghttp3_conn_set_client_stream_priority(
          *this, stream.id(), reinterpret_cast<const uint8_t*>(buf), len);
    }
  }

  StreamPriorityResult GetStreamPriority(const Stream& stream) override {
    // nghttp3_conn_get_stream_priority is only available on the server
    // side, where it reflects the peer's requested priority (e.g., from
    // PRIORITY_UPDATE frames). Client-side priority is tracked by the
    // Stream itself and returned directly from GetPriority in streams.cc.
    if (!session().is_server()) {
      return {stream.priority(), stream.priority_flags()};
    }
    nghttp3_pri pri;
    if (nghttp3_conn_get_stream_priority(*this, &pri, stream.id()) == 0) {
      StreamPriority level;
      switch (pri.urgency) {
        case NGHTTP3_URGENCY_HIGH:
          level = StreamPriority::HIGH;
          break;
        case NGHTTP3_URGENCY_LOW:
          level = StreamPriority::LOW;
          break;
        default:
          level = StreamPriority::DEFAULT;
          break;
      }
      return {level,
              pri.inc ? StreamPriorityFlags::INCREMENTAL
                      : StreamPriorityFlags::NON_INCREMENTAL};
    }
    return {StreamPriority::DEFAULT, StreamPriorityFlags::NON_INCREMENTAL};
  }

  int GetStreamData(Session::StreamData* data) override {
    static_assert(
        sizeof(ngtcp2_vec) == sizeof(nghttp3_vec) &&
            alignof(ngtcp2_vec) == alignof(nghttp3_vec) &&
            offsetof(ngtcp2_vec, base) == offsetof(nghttp3_vec, base) &&
            offsetof(ngtcp2_vec, len) == offsetof(nghttp3_vec, len),
        "ngtcp2_vec and nghttp3_vec must have identical layout");
    Debug(&session(), "HTTP/3 application getting stream data");
    if (conn_ && session().max_data_left()) {
      // nghttp3 reports fin through an int out-param; bridge it to the bool.
      int fin = 0;
      ssize_t ret =
          nghttp3_conn_writev_stream(*this,
                                     &data->id,
                                     &fin,
                                     reinterpret_cast<nghttp3_vec*>(data->data),
                                     kMaxVectorCount);
      // A negative return value indicates an error.
      if (ret < 0) {
        return static_cast<int>(ret);
      }
      data->fin = fin != 0;

      data->count = static_cast<size_t>(ret);
      if (data->id >= 0 && data->id != control_stream_id_ &&
          data->id != qpack_dec_stream_id_ &&
          data->id != qpack_enc_stream_id_) {
        data->stream = session().FindStream(data->id);
      }
    }

    return 0;
  }

  bool StreamCommit(Session::StreamData* data, size_t datalen) override {
    Debug(&session(),
          "HTTP/3 application committing stream %" PRIi64 " data %zu",
          data->id,
          datalen);
    // datalen is the total framed bytes consumed by ngtcp2, which includes
    // H3 frame overhead (HEADERS frame bytes, DATA frame type/length).
    // nghttp3 tracks its own offset via add_write_offset.
    int err = nghttp3_conn_add_write_offset(*this, data->id, datalen);
    if (err != 0) {
      session().SetApplicationError(nghttp3_err_infer_quic_app_error_code(err));
      return false;
    }
    // Raw application bytes are committed to the stream's outbound
    // immediately in on_read_data_callback (so that re-entrant
    // fill_outq calls see the advanced position). We only need to
    // propagate the fin flag here.
    if (data->stream && data->fin) {
      data->stream->Commit(0, true);
    }
    // After body data is committed, if on_read_data_callback signaled
    // EOF+NO_END_STREAM (trailers pending), emit the want-trailers
    // event to JS. This runs outside the NgHttp3CallbackScope so it's
    // safe to call into JS. The JS handler calls sendTrailers() which
    // calls nghttp3_conn_submit_trailers, queuing the TRAILERS frame
    // for the next writev_stream in the send loop.
    if (pending_trailers_stream_ == data->id) {
      pending_trailers_stream_ = -1;
      if (data->stream) EmitWantTrailers(*data->stream);
    }
    return true;
  }

  SET_NO_MEMORY_INFO()
  SET_MEMORY_INFO_NAME(Http3ApplicationImpl)
  SET_SELF_SIZE(Http3ApplicationImpl)

 private:
  inline operator nghttp3_conn*() const {
    DCHECK_NOT_NULL(conn_.get());
    return conn_.get();
  }

  inline bool is_control_stream(stream_id id) const {
    return id == control_stream_id_ || id == qpack_dec_stream_id_ ||
           id == qpack_enc_stream_id_;
  }

  bool SubmitHeaders(Stream& stream,
                     HeadersKind kind,
                     const Local<Array>& headers,
                     HeadersFlags flags) {
    Http3Headers nva(env(), headers);

    switch (kind) {
      case HeadersKind::HINTS: {
        if (!session().is_server()) return false;
        Debug(&session(),
              "Submitting %" PRIu64 " early hints for stream %" PRIu64,
              stream.id());
        return nghttp3_conn_submit_info(
                   *this, stream.id(), nva.data(), nva.length()) == 0;
      }
      case HeadersKind::INITIAL: {
        static constexpr nghttp3_data_reader reader = {on_read_data_callback};
        const nghttp3_data_reader* reader_ptr = nullptr;
        if (flags != HeadersFlags::TERMINAL) reader_ptr = &reader;

        if (session().is_server()) {
          Debug(&session(),
                "Submitting %" PRIu64 " response headers for stream %" PRIu64,
                nva.length(),
                stream.id());
          return nghttp3_conn_submit_response(*this,
                                              stream.id(),
                                              nva.data(),
                                              nva.length(),
                                              reader_ptr) == 0;
        }

        Debug(&session(),
              "Submitting %" PRIu64 " request headers for stream %" PRIu64,
              nva.length(),
              stream.id());
        return nghttp3_conn_submit_request(*this,
                                           stream.id(),
                                           nva.data(),
                                           nva.length(),
                                           reader_ptr,
                                           &stream) == 0;
      }
      case HeadersKind::TRAILING: {
        Debug(&session(),
              "Submitting %" PRIu64 " trailing headers for stream %" PRIu64,
              nva.length(),
              stream.id());
        return nghttp3_conn_submit_trailers(
                   *this, stream.id(), nva.data(), nva.length()) == 0;
      }
    }

    return false;
  }

  Http3StreamState* GetStreamState(Stream& stream) const {
    return static_cast<Http3StreamState*>(stream.application_state());
  }

  Http3StreamState& GetOrCreateStreamState(Stream& stream) {
    if (stream.application_state() == nullptr) {
      stream.set_application_state(std::make_unique<Http3StreamState>());
    }
    return *GetStreamState(stream);
  }

  void BeginHeaders(Stream& stream, HeadersKind kind) {
    auto& state = GetOrCreateStreamState(stream);
    state.headers_length = 0;
    state.headers.clear();
    state.headers_kind = kind;
  }

  bool AddHeader(Stream& stream, std::unique_ptr<Http3Header> header) {
    auto& state = GetOrCreateStreamState(stream);
    size_t length = header->length();
    if (state.headers.size() >= options_.max_header_pairs ||
        state.headers_length + length > options_.max_header_length) {
      return false;
    }
    state.headers_length += length;
    state.headers.push_back(std::move(header));
    return true;
  }

  void EmitHeaders(Stream& stream) {
    auto& state = GetOrCreateStreamState(stream);
    stream.RecordReceivedActivity();
    if (!env()->can_call_into_js() || !state.wants_headers) {
      state.headers.clear();
      return;
    }

    CallbackScope<Stream> cb_scope(&stream);
    auto& binding = BindingData::Get(env());
    size_t count = state.headers.size() * 2;
    LocalVector<Value> values(env()->isolate(), count);

    for (size_t i = 0; i < state.headers.size(); i++) {
      Local<Value> name;
      Local<Value> value;
      if (!state.headers[i]->GetName(&binding).ToLocal(&name) ||
          !state.headers[i]->GetValue(&binding).ToLocal(&value)) [[unlikely]] {
        state.headers.clear();
        return;
      }
      values[i * 2] = name;
      values[i * 2 + 1] = value;
    }

    state.headers.clear();
    Local<Value> argv[] = {
        Array::New(env()->isolate(), values.data(), count),
        Integer::NewFromUnsigned(env()->isolate(),
                                 static_cast<uint32_t>(state.headers_kind))};
    stream.MakeCallback(
        binding.stream_headers_callback(), arraysize(argv), argv);
  }

  void EmitWantTrailers(Stream& stream) {
    auto* state = GetStreamState(stream);
    if (!env()->can_call_into_js() || state == nullptr ||
        !state->wants_trailers) {
      return;
    }
    CallbackScope<Stream> cb_scope(&stream);
    stream.MakeCallback(
        BindingData::Get(env()).stream_trailers_callback(), 0, nullptr);
  }

  void BuildOriginPayload() {
    // Build the serialized ORIGIN frame payload from the SNI configuration.
    // Each origin entry is: 2-byte BE length + origin string.
    // Wildcard ('*') entries and entries with authoritative=false are skipped.
    auto& sni = session().config().options.sni;
    for (auto& [hostname, opts] : sni) {
      if (hostname == "*" || !opts.authoritative) continue;
      std::string origin = "https://";
      origin += hostname;
      if (opts.port != 443) {
        origin += ":";
        origin += std::to_string(opts.port);
      }
      // 2-byte BE length prefix
      uint16_t len = static_cast<uint16_t>(origin.size());
      origin_payload_.push_back(static_cast<uint8_t>((len >> 8) & 0xff));
      origin_payload_.push_back(static_cast<uint8_t>(len & 0xff));
      // Origin string bytes
      origin_payload_.insert(
          origin_payload_.end(), origin.begin(), origin.end());
    }
    if (!origin_payload_.empty()) {
      origin_vec_ = {origin_payload_.data(), origin_payload_.size()};
    }
  }

  Http3ConnectionPointer InitializeConnection() {
    nghttp3_conn* conn = nullptr;
    nghttp3_settings settings = options_;
    if (!origin_payload_.empty()) {
      settings.origin_list = &origin_vec_;
    }
    if (session().is_server()) {
      CHECK_EQ(nghttp3_conn_server_new(
                   &conn, &kCallbacks, &settings, allocator_, this),
               0);
    } else {
      CHECK_EQ(nghttp3_conn_client_new(
                   &conn, &kCallbacks, &settings, allocator_, this),
               0);
    }
    return Http3ConnectionPointer(conn);
  }

  void OnBeginHeaders(stream_id id) {
    auto stream = FindOrCreateStream(id);
    if (!stream) [[unlikely]]
      return;
    Debug(&session(),
          "HTTP/3 application beginning initial block of headers for stream "
          "%" PRIi64,
          id);
    BeginHeaders(*stream, HeadersKind::INITIAL);
  }

  void OnReceiveHeader(stream_id id, std::unique_ptr<Http3Header> header) {
    auto stream = session().FindStream(id);

    if (!stream) [[unlikely]]
      return;
    if (header->name() == ":status" && header->value()[0] == '1') {
      Debug(&session(),
            "HTTP/3 application switching to hints headers for stream %" PRIi64,
            stream->id());
      GetOrCreateStreamState(*stream).headers_kind = HeadersKind::HINTS;
    }
    IF_QUIC_DEBUG(env()) {
      Debug(&session(),
            "Received header \"%s: %s\"",
            header->name(),
            header->value());
    }
    AddHeader(*stream, std::move(header));
  }

  void OnEndHeaders(stream_id id, int fin) {
    auto stream = session().FindStream(id);
    if (!stream) [[unlikely]]
      return;
    Debug(&session(),
          "HTTP/3 application received end of headers for stream %" PRIi64,
          id);
    EmitHeaders(*stream);
    // EmitHeaders calls into JavaScript, which can synchronously destroy the
    // stream. Its arena-backed state is released by Destroy(), so do not touch
    // the stream again if that happened.
    if (stream->is_destroyed()) return;
    if (fin) {
      // The stream is done. There's no more data to receive!
      Debug(&session(), "Headers are final for stream %" PRIi64, id);
      Stream::ReceiveDataFlags flags{
          .fin = true,
          .early = false,
      };
      stream->ReceiveData(nullptr, 0, flags);
    }
  }

  void OnBeginTrailers(stream_id id) {
    auto stream = FindOrCreateStream(id);
    if (!stream) [[unlikely]]
      return;
    Debug(&session(),
          "HTTP/3 application beginning block of trailers for stream %" PRIi64,
          id);
    BeginHeaders(*stream, HeadersKind::TRAILING);
  }

  void OnReceiveTrailer(stream_id id, std::unique_ptr<Http3Header> header) {
    auto stream = session().FindStream(id);
    if (!stream) [[unlikely]]
      return;
    IF_QUIC_DEBUG(env()) {
      Debug(&session(),
            "Received header \"%s: %s\"",
            header->name(),
            header->value());
    }
    AddHeader(*stream, std::move(header));
  }

  void OnEndTrailers(stream_id id, int fin) {
    auto stream = session().FindStream(id);
    if (!stream) [[unlikely]]
      return;
    Debug(&session(),
          "HTTP/3 application received end of trailers for stream %" PRIi64,
          id);
    EmitHeaders(*stream);
    // EmitHeaders calls into JavaScript, which can synchronously destroy the
    // stream. Its arena-backed state is released by Destroy(), so do not touch
    // the stream again if that happened.
    if (stream->is_destroyed()) return;
    if (fin) {
      Debug(&session(), "Trailers are final for stream %" PRIi64, id);
      Stream::ReceiveDataFlags flags{
          .fin = true,
          .early = false,
      };
      stream->ReceiveData(nullptr, 0, flags);
    }
  }

  void OnEndStream(stream_id id) {
    auto stream = session().FindStream(id);
    if (!stream) [[unlikely]]
      return;
    Debug(&session(),
          "HTTP/3 application received end of stream for stream %" PRIi64,
          id);
    Stream::ReceiveDataFlags flags{
        .fin = true,
        .early = false,
    };
    stream->ReceiveData(nullptr, 0, flags);
  }

  void OnSendStopSending(stream_id id, error_code app_error_code) {
    auto stream = session().FindStream(id);
    if (!stream) [[unlikely]]
      return;
    Debug(&session(),
          "HTTP/3 application should send stop sending for stream %" PRIi64,
          id);
    stream->SendStopSending(app_error_code);
  }

  void OnDoResetStream(stream_id id, error_code app_error_code) {
    auto stream = session().FindStream(id);
    if (!stream) [[unlikely]]
      return;
    Debug(&session(),
          "HTTP/3 application received a request to reset stream for stream "
          "%" PRIi64,
          id);
    stream->DoStreamReset(app_error_code);
  }

  void OnShutdown(stream_id id) {
    // The peer has sent a GOAWAY frame. This callback fires inside
    // NgHttp3CallbackScope, so we cannot call into JS, destroy streams,
    // or enter Close(GRACEFUL) here (which could trigger FinishClose and
    // deferred destroy, preventing PostReceive from running).
    //
    // Store the GOAWAY stream ID — PostReceive() handles everything
    // outside all callback scopes. For the shutdown notice (first phase,
    // sentinel ID), we still store it so PostReceive knows to enter
    // graceful close mode. For the final GOAWAY (real stream ID), we
    // overwrite with the lower value.
    Debug(&session(), "HTTP/3 received GOAWAY (id=%" PRIi64 ")", id);
    pending_goaway_id_ = id;
  }

  void PostReceive() override {
    if (pending_goaway_id_ < 0) return;
    stream_id goaway_id = pending_goaway_id_;
    pending_goaway_id_ = -1;

    bool is_notice =
        static_cast<uint64_t>(goaway_id) >= NGHTTP3_SHUTDOWN_NOTICE_STREAM_ID;

    // For the shutdown notice, replace the sentinel stream ID with -1
    // so JS sees a clean marker instead of a huge implementation detail.
    stream_id emit_id = is_notice ? -1 : goaway_id;

    if (!is_notice) {
      // Final GOAWAY: destroy client-initiated bidi streams with
      // IDs > goaway_id. These were not processed by the peer and
      // can be retried. Copy the map because Destroy modifies it.
      auto streams = session().streams();
      for (auto& [id, stream] : streams) {
        if (session().is_destroyed()) return;
        if (ngtcp2_is_bidi_stream(id) && id > goaway_id) {
          stream->Destroy(
              QuicError::ForApplication(NGHTTP3_H3_REQUEST_REJECTED));
        }
      }
      if (session().is_destroyed()) return;
    }

    // Notify JS for both notice and final GOAWAY. The notice uses
    // -1 to signal "server is shutting down, stop new requests" without
    // implying any specific stream boundary. The final GOAWAY (if it
    // arrives separately) provides the exact stream ID for retry decisions.
    //
    // We do NOT call Close(GRACEFUL) here. The JS ongoaway handler sets
    // isPendingClose (preventing new streams). The session closes naturally
    // when the peer sends CONNECTION_CLOSE after all streams finish.
    // Calling Close(GRACEFUL) would send a GOAWAY back and trigger
    // BeginShutdown, which can interfere with in-progress streams.
    session().EmitGoaway(emit_id);
  }

  void OnReceiveSettings(const nghttp3_proto_settings* settings) {
    options_.enable_connect_protocol = settings->enable_connect_protocol;
    options_.enable_datagrams = settings->h3_datagram;
    options_.max_field_section_size = settings->max_field_section_size;
    options_.qpack_blocked_streams = settings->qpack_blocked_streams;
    options_.qpack_max_dtable_capacity = settings->qpack_max_dtable_capacity;

    // Per RFC 9297 §3, an H3 endpoint MUST NOT send HTTP Datagrams
    // unless the peer indicated support via SETTINGS_H3_DATAGRAM=1.
    // If the peer disabled it, set the session's max datagram size to 0
    // which blocks sends at the existing JS/C++ check.
    if (!settings->h3_datagram) {
      session().set_max_datagram_size(0);
    }

    Debug(&session(),
          "HTTP/3 application received updated settings: %s",
          options_);
    // The settings are part of the application
    session().EmitApplication();
  }

  bool started_ = false;
  nghttp3_mem* allocator_;
  Options options_;
  Http3ConnectionPointer conn_;
  stream_id control_stream_id_ = -1;
  stream_id qpack_dec_stream_id_ = -1;
  stream_id qpack_enc_stream_id_ = -1;

  // Set by on_read_data_callback when EOF+NO_END_STREAM (trailers pending).
  // Consumed by StreamCommit to trigger EmitWantTrailers outside the
  // nghttp3 callback scope.
  stream_id pending_trailers_stream_ = -1;

  // Set by OnShutdown when the peer sends a final GOAWAY. Consumed by
  // PostReceive() outside all callback scopes to destroy rejected
  // streams and notify JS.
  stream_id pending_goaway_id_ = -1;

  // ORIGIN frame support (RFC 9412).
  // origin_payload_ holds the serialized ORIGIN frame payload for sending.
  // origin_vec_ points into origin_payload_ for nghttp3_settings.origin_list.
  // received_origins_ accumulates origins from received ORIGIN frames.
  std::vector<uint8_t> origin_payload_;
  nghttp3_vec origin_vec_{nullptr, 0};
  std::vector<std::string> received_origins_;

  // ==========================================================================
  // Static callbacks

  static Http3ApplicationImpl* From(nghttp3_conn* conn, void* user_data) {
    DCHECK_NOT_NULL(user_data);
    auto app = static_cast<Http3ApplicationImpl*>(user_data);
    DCHECK_EQ(conn, app->conn_.get());
    return app;
  }

  // Cache the Stream* in nghttp3 so we can quickly get it later:
  void BindStreamUserData(stream_id id, Stream* stream) {
    if (conn_) nghttp3_conn_set_stream_user_data(*this, id, stream);
  }

  BaseObjectWeakPtr<Stream> FindOrCreateStream(stream_id id) {
    if (auto stream = session().FindStream(id)) {
      BindStreamUserData(id, stream.get());
      return stream;
    }
    // No record of a locally-initiated stream means we already destroyed it,
    // and frames still in flight must not bring it back to life. See
    // DefaultApplication::ReceiveStreamData for the same guard on the raw
    // QUIC path.
    if (!session().is_destroyed() &&
        ngtcp2_conn_is_local_stream(session(), id)) {
      return {};
    }
    if (auto stream = session().CreateStream(id)) {
      if (!stream->is_destroyed()) BindStreamUserData(id, stream.get());
      return stream;
    }
    return {};
  }

#define NGHTTP3_CALLBACK_SCOPE(name)                                           \
  auto ptr = From(conn, conn_user_data);                                       \
  CHECK_NOT_NULL(ptr);                                                         \
  auto& name = *ptr;                                                           \
  NgHttp3CallbackScope scope(&name.session());

  static nghttp3_ssize on_read_data_callback(nghttp3_conn* conn,
                                             stream_id id,
                                             nghttp3_vec* vec,
                                             size_t veccnt,
                                             uint32_t* pflags,
                                             void* conn_user_data,
                                             void* stream_user_data) {
    auto ptr = From(conn, conn_user_data);
    CHECK_NOT_NULL(ptr);
    auto& app = *ptr;
    NgHttp3CallbackScope scope(&app.session());

    BaseObjectPtr<Stream> stream(static_cast<Stream*>(stream_user_data));
    if (!stream) [[unlikely]] {
      stream = app.session().FindStream(id);
      if (!stream) return NGHTTP3_ERR_CALLBACK_FAILURE;
    }

    if (stream->is_eos()) {
      *pflags |= NGHTTP3_DATA_FLAG_EOF;
      if (app.GetOrCreateStreamState(*stream).wants_trailers) {
        *pflags |= NGHTTP3_DATA_FLAG_NO_END_STREAM;
        app.pending_trailers_stream_ = id;
      }
      return 0;
    }

    size_t max_count = std::min(veccnt, static_cast<size_t>(kMaxVectorCount));
    nghttp3_ssize result = 0;

    auto next =
        [&](int status, const ngtcp2_vec* data, size_t count, bob::Done done) {
          switch (status) {
            case bob::Status::STATUS_BLOCK:
            case bob::Status::STATUS_WAIT:
              result = NGHTTP3_ERR_WOULDBLOCK;
              return;
            case bob::Status::STATUS_EOS:
              *pflags |= NGHTTP3_DATA_FLAG_EOF;
              if (app.GetOrCreateStreamState(*stream).wants_trailers) {
                *pflags |= NGHTTP3_DATA_FLAG_NO_END_STREAM;
                app.pending_trailers_stream_ = id;
              }
              break;
          }
          count = std::min(count, max_count);
          // nghttp3 requires read_data to return either data (count > 0),
          // EOF, or WOULDBLOCK. A STATUS_CONTINUE with 0 vecs means the
          // outbound has no uncommitted data right now (e.g., all data was
          // already committed on a previous call, or the DataQueue is empty
          // but not yet capped). Map this to WOULDBLOCK so nghttp3 sets
          // READ_DATA_BLOCKED and waits for ResumeStream.
          if (count == 0 && !((*pflags) & NGHTTP3_DATA_FLAG_EOF)) {
            result = NGHTTP3_ERR_WOULDBLOCK;
            return;
          }
          size_t raw_bytes = 0;
          for (size_t n = 0; n < count; n++) {
            vec[n].base = data[n].base;
            vec[n].len = data[n].len;
            raw_bytes += data[n].len;
          }
          // Commit the raw application bytes immediately so that the
          // next Pull (if fill_outq re-enters read_data) sees the
          // advanced position. Commit only moves the offset — the
          // underlying buffers stay valid until Acknowledge.
          if (raw_bytes > 0) {
            stream->Commit(raw_bytes);
          }
          result = static_cast<nghttp3_ssize>(count);
        };

    ngtcp2_vec data[kMaxVectorCount];
    stream->Pull(std::move(next),
                 bob::Options::OPTIONS_SYNC,
                 data,
                 max_count,
                 max_count);

    return result;
  }

  static int on_acked_stream_data(nghttp3_conn* conn,
                                  stream_id id,
                                  uint64_t datalen,
                                  void* conn_user_data,
                                  void* stream_user_data) {
    // This callback is invoked by nghttp3_conn_add_ack_offset() (called
    // from Http3ApplicationImpl::AcknowledgeStreamData). We must NOT call
    // AcknowledgeStreamData here — that would re-enter nghttp3 via
    // nghttp3_conn_add_ack_offset, triggering the NgHttp3CallbackScope
    // re-entrancy assertion. Instead, directly notify the stream that data
    // was acknowledged, which is what the base Application implementation
    // does.
    auto ptr = From(conn, conn_user_data);
    CHECK_NOT_NULL(ptr);
    auto& app = *ptr;
    BaseObjectPtr<Stream> stream(static_cast<Stream*>(stream_user_data));
    if (!stream) [[unlikely]] {
      stream = app.session().FindStream(id);
    }
    if (stream) {
      stream->Acknowledge(static_cast<size_t>(datalen));
    }
    return NGTCP2_SUCCESS;
  }

  static int on_receive_data(nghttp3_conn* conn,
                             stream_id id,
                             const uint8_t* data,
                             size_t datalen,
                             void* conn_user_data,
                             void* stream_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    // The on_receive_data callback will never be called for control streams,
    // so we know that if we get here, the data received is for a stream that
    // we know is for an HTTP payload.
    if (app.is_control_stream(id)) [[unlikely]] {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
    // A cached Stream* is cleared before the Stream is removed from the
    // session, non-null here means the stream is good to go.
    if (auto* cached = static_cast<Stream*>(stream_user_data)) [[likely]] {
      BaseObjectPtr<Stream> stream(cached);
      stream->ReceiveData(data, datalen, Stream::ReceiveDataFlags{});
      return NGTCP2_SUCCESS;
    }

    auto& session = app.session();

    // DATA frames for a request stream the application already destroyed can
    // still arrive. Drop the payload rather than resurrecting the stream or
    // tearing down the connection, but return its credit: unlike framing
    // bytes, DATA payload is not included in the count nghttp3 reports to
    // ReceiveStreamData, so we own it. The is_destroyed() check must come
    // first, see DefaultApplication::ReceiveStreamData.
    if (!session.is_destroyed() && !session.FindStream(id) &&
        ngtcp2_conn_is_local_stream(session, id)) {
      Debug(&session,
            "HTTP/3 discarding %zu bytes for destroyed local stream %" PRIi64,
            datalen,
            id);
      app.ReturnConnectionCredit(datalen);
      return NGTCP2_SUCCESS;
    }

    if (auto stream = app.FindOrCreateStream(id)) {
      stream->ReceiveData(data, datalen, Stream::ReceiveDataFlags{});
      return NGTCP2_SUCCESS;
    }
    return NGHTTP3_ERR_CALLBACK_FAILURE;
  }

  static int on_deferred_consume(nghttp3_conn* conn,
                                 stream_id id,
                                 size_t consumed,
                                 void* conn_user_data,
                                 void* stream_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    Debug(&app.session(),
          "HTTP/3 application deferred consume %zu bytes",
          consumed);
    app.session().Consume(id, consumed);
    return NGTCP2_SUCCESS;
  }

  static int on_begin_headers(nghttp3_conn* conn,
                              stream_id id,
                              void* conn_user_data,
                              void* stream_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    if (app.is_control_stream(id)) [[unlikely]] {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
    app.OnBeginHeaders(id);
    return NGTCP2_SUCCESS;
  }

  static int on_receive_header(nghttp3_conn* conn,
                               stream_id id,
                               int32_t token,
                               nghttp3_rcbuf* name,
                               nghttp3_rcbuf* value,
                               uint8_t flags,
                               void* conn_user_data,
                               void* stream_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    if (app.is_control_stream(id)) [[unlikely]] {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
    if (Http3Header::IsZeroLength(token, name, value)) return NGTCP2_SUCCESS;
    app.OnReceiveHeader(
        id,
        std::make_unique<Http3Header>(app.env(), token, name, value, flags));
    return NGTCP2_SUCCESS;
  }

  static int on_end_headers(nghttp3_conn* conn,
                            stream_id id,
                            int fin,
                            void* conn_user_data,
                            void* stream_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    if (app.is_control_stream(id)) [[unlikely]] {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
    app.OnEndHeaders(id, fin);
    return NGTCP2_SUCCESS;
  }

  static int on_begin_trailers(nghttp3_conn* conn,
                               stream_id id,
                               void* conn_user_data,
                               void* stream_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    if (app.is_control_stream(id)) [[unlikely]] {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
    app.OnBeginTrailers(id);
    return NGTCP2_SUCCESS;
  }

  static int on_receive_trailer(nghttp3_conn* conn,
                                stream_id id,
                                int32_t token,
                                nghttp3_rcbuf* name,
                                nghttp3_rcbuf* value,
                                uint8_t flags,
                                void* conn_user_data,
                                void* stream_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    if (app.is_control_stream(id)) [[unlikely]] {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
    if (Http3Header::IsZeroLength(token, name, value)) return NGTCP2_SUCCESS;
    app.OnReceiveTrailer(
        id,
        std::make_unique<Http3Header>(app.env(), token, name, value, flags));
    return NGTCP2_SUCCESS;
  }

  static int on_end_trailers(nghttp3_conn* conn,
                             stream_id id,
                             int fin,
                             void* conn_user_data,
                             void* stream_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    if (app.is_control_stream(id)) [[unlikely]] {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
    app.OnEndTrailers(id, fin);
    return NGTCP2_SUCCESS;
  }

  static int on_end_stream(nghttp3_conn* conn,
                           stream_id id,
                           void* conn_user_data,
                           void* stream_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    if (app.is_control_stream(id)) [[unlikely]] {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
    app.OnEndStream(id);
    return NGTCP2_SUCCESS;
  }

  static int on_send_stop_sending(nghttp3_conn* conn,
                                  stream_id id,
                                  error_code app_error_code,
                                  void* conn_user_data,
                                  void* stream_user_data) {
    // this callback asks the app side to send a stop sending
    NGHTTP3_CALLBACK_SCOPE(app);
    if (app.is_control_stream(id)) [[unlikely]] {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
    app.OnSendStopSending(id, app_error_code);
    return NGTCP2_SUCCESS;
  }

  static int on_do_reset_stream(nghttp3_conn* conn,
                                stream_id id,
                                error_code app_error_code,
                                void* conn_user_data,
                                void* stream_user_data) {
    // this callback ask the app side to do a reset stream
    NGHTTP3_CALLBACK_SCOPE(app);
    if (app.is_control_stream(id)) [[unlikely]] {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
    app.OnDoResetStream(id, app_error_code);
    return NGTCP2_SUCCESS;
  }

  static int on_shutdown(nghttp3_conn* conn,
                         stream_id id,
                         void* conn_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    app.OnShutdown(id);
    return NGTCP2_SUCCESS;
  }

  static int on_receive_settings(nghttp3_conn* conn,
                                 const nghttp3_proto_settings* settings,
                                 void* conn_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    app.OnReceiveSettings(settings);
    return NGTCP2_SUCCESS;
  }

  static int on_receive_origin(nghttp3_conn* conn,
                               const uint8_t* origin,
                               size_t originlen,
                               void* conn_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    app.received_origins_.emplace_back(reinterpret_cast<const char*>(origin),
                                       originlen);
    return NGTCP2_SUCCESS;
  }

  static int on_end_origin(nghttp3_conn* conn, void* conn_user_data) {
    NGHTTP3_CALLBACK_SCOPE(app);
    if (!app.received_origins_.empty()) {
      app.session().EmitOrigins(std::move(app.received_origins_));
      app.received_origins_.clear();
    }
    return NGTCP2_SUCCESS;
  }

  static void on_rand(uint8_t* dest, size_t destlen) {
    CHECK(ncrypto::CSPRNG(dest, destlen));
  }

  static constexpr nghttp3_callbacks kCallbacks = {
      on_acked_stream_data,
      nullptr,  // stream_close (deprecated, use v2 below)
      on_receive_data,
      on_deferred_consume,
      on_begin_headers,
      on_receive_header,
      on_end_headers,
      on_begin_trailers,
      on_receive_trailer,
      on_end_trailers,
      on_send_stop_sending,
      on_end_stream,
      on_do_reset_stream,
      on_shutdown,
      nullptr,  // recv_settings (deprecated, use v2 below)
      on_receive_origin,
      on_end_origin,
      on_rand,
      on_receive_settings,
      // We don't have to listen for stream_close - nghttp3 only closes when
      // ReceiveStreamClose requests it, when we've already handled this.
      nullptr};
};

std::unique_ptr<Session::Application> CreateHttp3Application(
    Session* session, const Session::Application_Options& options) {
  Debug(session, "Selecting HTTP/3 application");
  return std::make_unique<Http3ApplicationImpl>(session, options);
}

}  // namespace quic
}  // namespace node
#endif  // OPENSSL_NO_QUIC