Skip to content

Commit

Permalink
quic: add applicationOptions to session
Browse files Browse the repository at this point in the history
Signed-off-by: James M Snell <jasnell@gmail.com>
PR-URL: #63267
Backport-PR-URL: #64675
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
  • Loading branch information
jasnell authored and aduh95 committed Aug 6, 2026
1 parent 4c00ab9 commit 42aa3f6
Show file tree
Hide file tree
Showing 11 changed files with 320 additions and 71 deletions.
101 changes: 78 additions & 23 deletions doc/api/quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,18 @@ added: v23.8.0

A `QuicSession` represents the local side of a QUIC connection.

### `session.applicationOptions`

<!-- YAML
added: REPLACEME
-->

* Type: {quic.ApplicationOptions}

The current application-level options for this session. These include settings
that are specific to the negotiated application protocol (e.g. HTTP/3) and may
be negotiated separately from the transport parameters. Read only.

### `session.close([options])`

<!-- YAML
Expand Down Expand Up @@ -2218,6 +2230,70 @@ added: v23.8.0

## Types

### type: `ApplicationOptions`

<!-- YAML
added: REPLACEME
-->

* Type: {Object}

The application specific options.

#### `applicationOptions.maxHeaderPairs`

* Type: {bigint|number}

Maximum number of header name-value pairs accepted per header block.
Headers beyond this limit are silently dropped. **Default:** `128`

#### `applicationOptions.maxHeaderLength`

* Type: {bigint|number}

Maximum total byte length of all header names and values combined per header
block. Headers that would push the total over this limit are silently
dropped. **Default:** `8192`

#### `applicationOptions.maxFieldSectionSize`

* Type: {bigint|number}

Maximum size of a compressed header field section (QPACK). `0` means
unlimited. **Default:** `0`

#### `applicationOptions.qpackMaxDTableCapacity`

* Type: {bigint|number}

QPACK dynamic table capacity in bytes. Set to `0` to disable the dynamic
table. **Default:** `4096`

#### `applicationOptions.qpackEncoderMaxDTableCapacity`

* Type: {bigint|number}

QPACK encoder maximum dynamic table capacity. **Default:** `4096`

#### `applicationOptions.qpackBlockedStreams`

* Type: {bigint|number}

Maximum number of streams that can e blocked waiting for QPACK dynamic table
updates. **Default:** `100`

#### `applicationOptions.enableConnectProtocol`

* Type: {boolean}

Enable the extended CONNECT protocol (RFC 9220). **Default:** `false`

#### `applicationOptions.enableDatagrams`

* Type: {boolean}

Enable HTTP/3 datagrams (RFC 9297). **Default:** `false`

### Type: `EndpointOptions`

<!-- YAML
Expand Down Expand Up @@ -2474,30 +2550,9 @@ Default: `'h3'`
added: REPLACEME
-->

* Type: {Object}
* Type: {quic.ApplicationOptions}

HTTP/3 application-specific options. These only apply when the negotiated
ALPN selects the HTTP/3 application (`'h3'`).

* `maxHeaderPairs` {number} Maximum number of header name-value pairs
accepted per header block. Headers beyond this limit are silently
dropped. **Default:** `128`
* `maxHeaderLength` {number} Maximum total byte length of all header
names and values combined per header block. Headers that would push
the total over this limit are silently dropped. **Default:** `8192`
* `maxFieldSectionSize` {number} Maximum size of a compressed header
field section (QPACK). `0` means unlimited. **Default:** `0`
* `qpackMaxDTableCapacity` {number} QPACK dynamic table capacity in
bytes. Set to `0` to disable the dynamic table. **Default:** `4096`
* `qpackEncoderMaxDTableCapacity` {number} QPACK encoder maximum
dynamic table capacity. **Default:** `4096`
* `qpackBlockedStreams` {number} Maximum number of streams that can
be blocked waiting for QPACK dynamic table updates.
**Default:** `100`
* `enableConnectProtocol` {boolean} Enable the extended CONNECT
protocol (RFC 9220). **Default:** `false`
* `enableDatagrams` {boolean} Enable HTTP/3 datagrams (RFC 9297).
**Default:** `false`
Application-specific options.

```mjs
const { listen } = await import('node:quic');
Expand Down
8 changes: 8 additions & 0 deletions lib/internal/quic/quic.js
Original file line number Diff line number Diff line change
Expand Up @@ -2678,6 +2678,14 @@ class QuicSession {
debug('session created');
}

get applicationOptions() {
// We don't cache application options because they may be updated by the
// C++ layer after session creation depending on the behavior of the
// application.
if (this.destroyed) return null;
return this.#handle.applicationOptions();
}

get localTransportParams() {
if (this.#inner.localTransportParams !== undefined) {
return this.#inner.localTransportParams;
Expand Down
49 changes: 48 additions & 1 deletion src/quic/application.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@

namespace node {

using v8::BigInt;
using v8::Boolean;
using v8::DictionaryTemplate;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Object;
using v8::Value;
Expand Down Expand Up @@ -114,6 +118,44 @@ Maybe<Session::Application_Options> Session::Application_Options::From(
return Just<Application_Options>(options);
}

MaybeLocal<Object> Session::Application_Options::ToObject(
Environment* env) const {
auto& binding_data = BindingData::Get(env);
auto tmpl = binding_data.application_options_template();
static constexpr std::string_view names[] = {
"maxHeaderPairs",
"maxHeaderLength",
"maxFieldSectionSize",
"qpackMaxDtableCapacity",
"qpackEncoderMaxDtableCapacity",
"qpackBlockedStreams",
"enableConnectProtocol",
"enableDatagrams",
};
if (tmpl.IsEmpty()) {
tmpl = DictionaryTemplate::New(env->isolate(), names);
binding_data.set_application_options_template(tmpl);
}
MaybeLocal<Value> values[] = {
BigInt::NewFromUnsigned(env->isolate(), max_header_pairs),
BigInt::NewFromUnsigned(env->isolate(), max_header_length),
BigInt::NewFromUnsigned(env->isolate(), max_field_section_size),
BigInt::NewFromUnsigned(env->isolate(), qpack_max_dtable_capacity),
BigInt::NewFromUnsigned(env->isolate(),
qpack_encoder_max_dtable_capacity),
BigInt::NewFromUnsigned(env->isolate(), qpack_blocked_streams),
Boolean::New(env->isolate(), enable_connect_protocol),
Boolean::New(env->isolate(), enable_datagrams),
};
static_assert(std::size(values) == std::size(names));

auto obj = tmpl->NewInstance(env->context(), values);
if (obj->SetPrototypeV2(env->context(), Null(env->isolate())).IsNothing()) {
return {};
}
return obj;
}

// ============================================================================

std::string Session::Application::StreamData::ToString() const {
Expand Down Expand Up @@ -655,7 +697,10 @@ class DefaultApplication final : public Session::Application {
// Marked NOLINT because the cpp linter gets confused about this using
// statement not being sorted with the using v8 statements at the top
// of the namespace.
using Application::Application; // NOLINT
DefaultApplication(Session* session, const Options& options)
: Session::Application(session, options), options_(options) {}

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

Session::Application::Type type() const override {
return Session::Application::Type::DEFAULT;
Expand Down Expand Up @@ -827,6 +872,8 @@ class DefaultApplication final : public Session::Application {
}
}

Options options_;

Stream::Queue stream_queue_;
};

Expand Down
4 changes: 4 additions & 0 deletions src/quic/application.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class Session::Application : public MemoryRetainer {
Application(Session* session, const Options& options);
DISALLOW_COPY_AND_MOVE(Application)

// Get the active options for this application. These may differ from the
// options passed at construction time since some options can be negotiated.
virtual const Options& options() const = 0;

// The type of Application, exposed via the session state so JS
// can observe which Application was selected after ALPN negotiation.
// This is used primarily for testing/debugging.
Expand Down
12 changes: 11 additions & 1 deletion src/quic/bindingdata.cc
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,17 @@ void BindingData::set_transport_params_template(

Local<DictionaryTemplate> BindingData::transport_params_template() const {
return PersistentToLocal::Default(env()->isolate(),
transport_params_template_);
transport_params_template_);
}

void BindingData::set_application_options_template(
Local<DictionaryTemplate> tmpl) {
application_options_template_.Reset(env()->isolate(), tmpl);
}

Local<DictionaryTemplate> BindingData::application_options_template() const {
return PersistentToLocal::Default(env()->isolate(),
application_options_template_);
}

#define V(name, _) \
Expand Down
4 changes: 4 additions & 0 deletions src/quic/bindingdata.h
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ class BindingData final
void set_transport_params_template(v8::Local<v8::DictionaryTemplate> tmpl);
v8::Local<v8::DictionaryTemplate> transport_params_template() const;

void set_application_options_template(v8::Local<v8::DictionaryTemplate> tmpl);
v8::Local<v8::DictionaryTemplate> application_options_template() const;

#define V(name, _) \
void set_##name##_callback(v8::Local<v8::Function> fn); \
v8::Local<v8::Function> name##_callback() const;
Expand All @@ -325,6 +328,7 @@ class BindingData final
#undef V

v8::Global<v8::DictionaryTemplate> transport_params_template_;
v8::Global<v8::DictionaryTemplate> application_options_template_;

#define V(name, _) v8::Global<v8::Function> name##_callback_;
QUIC_JS_CALLBACKS(V)
Expand Down
2 changes: 2 additions & 0 deletions src/quic/http3.cc
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ class Http3ApplicationImpl final : public Session::Application {
session->set_priority_supported();
}

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

Session::Application::Type type() const override {
return Session::Application::Type::HTTP3;
}
Expand Down
23 changes: 23 additions & 0 deletions src/quic/session.cc
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) {
V(SendDatagram, sendDatagram, SIDE_EFFECT) \
V(LocalTransportParams, localTransportParams, NO_SIDE_EFFECT) \
V(RemoteTransportParams, remoteTransportParams, NO_SIDE_EFFECT) \
V(ApplicationOptions, applicationOptions, NO_SIDE_EFFECT)

struct Session::State final {
#define V(_, name, type) type name;
Expand Down Expand Up @@ -1195,6 +1196,24 @@ struct Session::Impl final : public MemoryRetainer {
args.GetReturnValue().Set(obj);
}
}

JS_METHOD(ApplicationOptions) {
auto env = Environment::GetCurrent(args);
Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.This());

Local<Object> obj;
if (!session->has_application()) {
// The application has not yet been selected (ALPN negotiation is not
// yet complete on the server) or the session has been destroyed. In
// either case, the application options are not available.
return args.GetReturnValue().SetUndefined();
}
auto& options = session->application().options();
if (options.ToObject(env).ToLocal(&obj)) {
args.GetReturnValue().Set(obj);
}
}
// Internal ngtcp2 callbacks

static int on_acknowledge_stream_data_offset(ngtcp2_conn* conn,
Expand Down Expand Up @@ -2082,6 +2101,10 @@ TLSSession& Session::tls_session() const {
return *tls_session_;
}

bool Session::has_application() const {
return !is_destroyed() && impl_->application_ != nullptr;
}

Session::Application& Session::application() const {
DCHECK(!is_destroyed());
DCHECK(impl_->application_);
Expand Down
3 changes: 3 additions & 0 deletions src/quic/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {

std::string ToString() const;

v8::MaybeLocal<v8::Object> ToObject(Environment* env) const;

static const Application_Options kDefault;
};

Expand Down Expand Up @@ -324,6 +326,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
uint32_t version() const;
Endpoint& endpoint() const;
TLSSession& tls_session() const;
bool has_application() const;
Application& application() const;
const Config& config() const;
const Options& options() const;
Expand Down
Loading

0 comments on commit 42aa3f6