From b8bd6e4171ea4804638fcfa26f32fbd590d4f3b7 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 20 Jun 2026 08:12:08 +0200 Subject: [PATCH 01/37] Import nghttp3 webtransport PR --- .../nghttp3/lib/includes/nghttp3/nghttp3.h | 321 ++++- .../nghttp3/lib/includes/nghttp3/version.h | 2 +- deps/ngtcp2/nghttp3/lib/nghttp3_conn.c | 1264 ++++++++++++++++- deps/ngtcp2/nghttp3/lib/nghttp3_conn.h | 43 +- deps/ngtcp2/nghttp3/lib/nghttp3_conv.c | 6 + deps/ngtcp2/nghttp3/lib/nghttp3_conv.h | 7 + deps/ngtcp2/nghttp3/lib/nghttp3_err.c | 9 + deps/ngtcp2/nghttp3/lib/nghttp3_frame.c | 38 + deps/ngtcp2/nghttp3/lib/nghttp3_frame.h | 76 + deps/ngtcp2/nghttp3/lib/nghttp3_http.c | 6 + deps/ngtcp2/nghttp3/lib/nghttp3_http.h | 2 + deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c | 15 +- deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c | 54 +- deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h | 8 +- deps/ngtcp2/nghttp3/lib/nghttp3_stream.c | 241 ++++ deps/ngtcp2/nghttp3/lib/nghttp3_stream.h | 49 + deps/ngtcp2/nghttp3/lib/nghttp3_wt.c | 95 ++ deps/ngtcp2/nghttp3/lib/nghttp3_wt.h | 86 ++ 18 files changed, 2235 insertions(+), 87 deletions(-) create mode 100644 deps/ngtcp2/nghttp3/lib/nghttp3_wt.c create mode 100644 deps/ngtcp2/nghttp3/lib/nghttp3_wt.h diff --git a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h index d3dd9217d4b0..39ba6602c5a7 100644 --- a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h +++ b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h @@ -335,6 +335,27 @@ typedef uint64_t nghttp3_duration; * that might generating excessive load. */ #define NGHTTP3_ERR_H3_EXCESSIVE_LOAD -610 +/** + * @macro + * + * :macro:`NGHTTP3_ERR_H3_MESSAGE_ERROR` indicates that HTTP message + * was malformed. + */ +#define NGHTTP3_ERR_H3_MESSAGE_ERROR -611 +/** + * @macro + * + * :macro:`NGHTTP3_ERR_WT_SESSION_GONE` indicates that WebTransport + * session was terminated or rejected. + */ +#define NGHTTP3_ERR_WT_SESSION_GONE -612 +/** + * @macro + * + * :macro:`NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED` indicates that + * buffering WebTransport data stream was rejected. + */ +#define NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED -613 /** * @macro * @@ -503,6 +524,38 @@ typedef uint64_t nghttp3_duration; * error code ``QPACK_DECODER_STREAM_ERROR``. */ #define NGHTTP3_QPACK_DECODER_STREAM_ERROR 0x0202 +/** + * @macro + * + * :macro:`NGHTTP3_WT_BUFFERED_STREAM_REJECTED` is WebTransport error + * code ``WT_BUFFERED_STREAM_REJECTED``. + */ +#define NGHTTP3_WT_BUFFERED_STREAM_REJECTED 0x3994BD84 +/** + * @macro + * + * :macro:`NGHTTP3_WT_SESSION_GONE` is WebTransport error code + * ``WT_SESSION_GONE``. + */ +#define NGHTTP3_WT_SESSION_GONE 0x170D7B68 +/** + * @macro + * + * :macro:`NGHTTP3_WT_ALPN_ERROR` is WebTransport error code + * ``WT_ALPN_ERROR``. + * + * https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15 + */ +#define NGHTTP3_WT_ALPN_ERROR 0x0817B3DD +/** + * @macro + * + * :macro:`NGHTTP3_WT_REQUIREMENTS_NOT_MET` is WebTransport error code + * ``WT_REQUIREMENTS_NOT_MET``. + * + * https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15 + */ +#define NGHTTP3_WT_REQUIREMENTS_NOT_MET 0x212C0D48 /** * @functypedef @@ -1899,6 +1952,16 @@ typedef struct nghttp3_settings { * .. version-added:: 1.13.0 */ nghttp3_qpack_indexing_strat qpack_indexing_strat; + /** + * :member:`wt_enabled`, if set to nonzero, enables WebTransport. + * + * https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15 + * + * TODO For client, it might be better to always enable + * WebTransport. Only draft version of client needs to send + * SETTINGS_WT_ENABLED. + */ + uint8_t wt_enabled; } nghttp3_settings; #define NGHTTP3_PROTO_SETTINGS_V1 1 @@ -1939,6 +2002,12 @@ typedef struct nghttp3_proto_settings { * Datagrams (see :rfc:`9297`). */ uint8_t h3_datagram; + /** + * :member:`wt_enabled`, if set to nonzero, enables WebTransport. + * + * https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15 + */ + uint8_t wt_enabled; } nghttp3_proto_settings; /** @@ -2285,11 +2354,11 @@ typedef int (*nghttp3_recv_settings2)(nghttp3_conn *conn, * invoked when a stream identified by |stream_id| is closed. If * :macro:`NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET` is set in * |flags|, |rx_app_error_code| is the QUIC application error code - * that shut down the receiving side of the stream. If + * received from the remote endpoint. If * :macro:`NGHTTP3_STREAM_CLOSE_FLAG_TX_APP_ERROR_CODE_SET` is set in * |flags|, |tx_app_error_code| is the QUIC application error code - * that shut down the sending side of the stream. No application code - * means that direction of stream is closed without any error. + * sent to the remote endpoint. No application code means that + * direction of stream is closed without any error. * * This callback should be used with `nghttp3_conn_close_stream2`. If * `nghttp3_conn_close_stream` is used, the app_error_code is set to @@ -2312,6 +2381,69 @@ typedef int (*nghttp3_stream_close2)(nghttp3_conn *conn, uint32_t flags, void *conn_user_data, void *stream_user_data); +/** + * @functypedef + * + * :type:`nghttp3_recv_wt_data` is a callback function which is + * invoked when data is received on WebTransport data stream. + * |session_id| is the WebTransport session ID. |stream_id| is the + * stream ID of the WebTransport data stream. |data| points to the + * received data, and its length is |datalen|. + * + * The application is responsible for increasing flow control credit + * (say, increasing by |datalen| bytes). + * + * The implementation of this callback must return 0 if it succeeds. + * Returning :macro:`NGHTTP3_ERR_CALLBACK_FAILURE` will return to the + * caller immediately. Any values other than 0 is treated as + * :macro:`NGHTTP3_ERR_CALLBACK_FAILURE`. + */ +typedef int (*nghttp3_recv_wt_data)(nghttp3_conn *conn, int64_t session_id, + int64_t stream_id, const uint8_t *data, + size_t datalen, void *conn_user_data, + void *stream_user_data); + +/** + * @functypedef + * + * :type:`nghttp3_wt_data_stream_open` is a callback function which is + * invoked when a remote stream denoted by |stream_id| is identified + * as WebTransport data stream that belongs to WebTransport session + * identified by |session_id|. This callback function is called after + * WebTransport session is confirmed. + * + * The implementation of this callback must return 0 if it succeeds. + * Returning :macro:`NGHTTP3_ERR_CALLBACK_FAILURE` will return to the + * caller immediately. Any values other than 0 is treated as + * :macro:`NGHTTP3_ERR_CALLBACK_FAILURE`. + */ +typedef int (*nghttp3_wt_data_stream_open)(nghttp3_conn *conn, + int64_t session_id, + int64_t stream_id, + void *conn_user_data, + void *stream_user_data); + +/** + * @functypedef + * + * :type:`nghttp3_recv_wt_close_session` is a callback function which + * is invoked when WT_CLOSE_SESSION Capsule is received. The + * WebTransport session is identified by |session_id|. + * |wt_error_code| is Application Error Code. The buffer pointed by + * |msg| of length |msglen| contains Application Error Message. + * + * The implementation of this callback must return 0 if it succeeds. + * Returning :macro:`NGHTTP3_ERR_CALLBACK_FAILURE` will return to the + * caller immediately. Any values other than 0 is treated as + * :macro:`NGHTTP3_ERR_CALLBACK_FAILURE`. + */ +typedef int (*nghttp3_recv_wt_close_session)(nghttp3_conn *conn, + int64_t session_id, + uint32_t wt_error_code, + const uint8_t *msg, size_t msglen, + void *conn_user_data, + void *stream_user_data); + #define NGHTTP3_CALLBACKS_V1 1 #define NGHTTP3_CALLBACKS_V2 2 #define NGHTTP3_CALLBACKS_V3 3 @@ -2462,6 +2594,22 @@ typedef struct nghttp3_callbacks { * .. version-added:: 1.18.0 */ nghttp3_stream_close2 stream_close2; + /** + * :member:`recv_wt_data` is a callback function which is invoked + * when data on WebTransport data stream is received. + */ + nghttp3_recv_wt_data recv_wt_data; + /** + * :member:`wt_data_stream_open` is a callback function which is + * invoked when a remote stream is identified as WebTransport data + * stream. + */ + nghttp3_wt_data_stream_open wt_data_stream_open; + /** + * :member:`recv_wt_close_session` is a callback function which is + * invoked when WT_CLOSE_SESSION Capsule is received. + */ + nghttp3_recv_wt_close_session recv_wt_close_session; } nghttp3_callbacks; /** @@ -2938,11 +3086,11 @@ NGHTTP3_EXTERN int nghttp3_conn_close_stream(nghttp3_conn *conn, * `nghttp3_conn_close_stream2` tells the library that a stream * identified by |stream_id| has been closed. If * :macro:`NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET` is set in - * |flags|, |rx_app_error_code| is the QUIC application error code - * that shut down the receiving side of the stream. Similarly, + * |flags|, |rx_app_error_code| is QUIC application error received + * from the remote endpoint. Similarly, * :macro:`NGHTTP3_STREAM_CLOSE_FLAG_TX_APP_ERROR_CODE_SET` is set in - * |flags|, |tx_app_error_code| is the QUIC application error code - * that shut down the sending side of the stream. + * |flags|, |tx_app_error_code| is QUIC application error sent to the + * remote endpoint. * * For stream close callback, prefer * :member:`nghttp3_callbacks.stream_close2` to @@ -3476,6 +3624,165 @@ NGHTTP3_EXTERN int nghttp3_conn_is_drained(nghttp3_conn *conn); */ NGHTTP3_EXTERN int nghttp3_conn_is_drained2(const nghttp3_conn *conn); +/** + * @function + * + * `nghttp3_conn_submit_wt_request` works like + * `nghttp3_conn_submit_request`, but it is specifically tailored for + * WebTransport session establishment. |nva| of length |nvlen| + * specifies HTTP request header fields. They must contain at least + * the following fields: + * + * - :method = "CONNECT" + * - :scheme = "https" + * - :protocol = "webtransport-h3" + * - :authority + * - :path + * + * The application must also set the following settings: + * + * - :member:`nghttp3_settings.h3_datagram` = 1 + * - :member:`nghttp3_settings.wt_enabled` = 1 + * + * It also must send the following QUIC transport parameters: + * + * - max_datagram_frame_size > 0 + * - reset_stream_at + * + * The application should wait for SETTINGS frame from server and make + * sure that it satisfies server-side requirements for WebTransport. + * + * After receiving 2xx response from server, WebTransport session is + * established. `nghttp3_conn_open_wt_data_stream` is used to open + * WebTransport data streams. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * TBD + */ +NGHTTP3_EXTERN int nghttp3_conn_submit_wt_request(nghttp3_conn *conn, + int64_t stream_id, + const nghttp3_nv *nva, + size_t nvlen, + void *stream_user_data); + +/** + * @function + * + * `nghttp3_conn_submit_wt_response` works like + * `nghttp3_conn_submit_response`, but it is specifically tailored for + * WebTransport session establishment. |nva| of length |nvlen| + * specifies HTTP response header fields. It must contain 2xx status + * code in :status field. + * + * The application should make sure that the stream denoted by + * |stream_id| is a request stream that requests WebTransport session + * establishment. If this function is called inside + * :member:`nghttp3_callbacks.end_headers` callback, + * `nghttp3_conn_server_confirm_wt_session` is called internally, and + * it establishes WebTransport session. If this function is called + * outside of the callback, the application must call + * `nghttp3_conn_server_confirm_wt_session` after calling this + * function. + * + * If `nghttp3_conn_submit_response` is used against the WebTransport + * upgrade request, it means refusal of the request regardless of HTTP + * status code. The application is responsible to set non-2xx status + * code when `nghttp3_conn_submit_response` is used. If + * `nghttp3_conn_submit_response` is called from + * :member:`nghttp3_callbacks.end_headers` callback, + * :member:`nghttp3_callbacks.stop_sending` callback is automatically + * called. If `nghttp3_conn_submit_response` is called outside of the + * :member:`nghttp3_callbacks.end_headers` callback, + * :member:`nghttp3_callbacks.stop_sending` is not called + * automatically. The application should tell QUIC stack to send + * STOP_SENDING frame to this stream. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * TBD + */ +NGHTTP3_EXTERN int nghttp3_conn_submit_wt_response(nghttp3_conn *conn, + int64_t stream_id, + const nghttp3_nv *nva, + size_t nvlen); + +/** + * @function + * + * `nghttp3_conn_server_confirm_wt_session` establishes WebTransport + * session. This should be called after + * `nghttp3_conn_submit_wt_response` call if it is not called inside + * `nghttp3_callbacks.end_headers` callback. + * + * Only server can call this function. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * TBD + */ +NGHTTP3_EXTERN int nghttp3_conn_server_confirm_wt_session(nghttp3_conn *conn, + int64_t session_id, + nghttp3_tstamp ts); + +/** + * @function + * + * `nghttp3_conn_open_wt_data_stream` opens WebTransport data stream. + * |session_id| is the stream ID that established WebTransport + * session. |stream_id| is the stream ID to write data, and it can be + * both bidirectional and unidirectional. |dr| must not be NULL, and + * it must have non-NULL callback. + * + * This function can be also used to start writing to the + * bidirectional stream initiated by the remote endpoint. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * TBD + */ +NGHTTP3_EXTERN int nghttp3_conn_open_wt_data_stream( + nghttp3_conn *conn, int64_t session_id, int64_t stream_id, + const nghttp3_data_reader *dr, void *stream_user_data); + +/** + * @function + * + * `nghttp3_conn_close_wt_session` closes WebTransport session denoted + * by |session_id| which is the stream ID that established + * WebTransport session. |wt_error_code| is WebTransport error code. + * Upon calling this function, all existing WebTransport data streams + * are shutdown. |msg| of |msglen| bytes is the application error + * message, which is optional. |msglen| must be less than or equal to + * 1024. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * TBD + */ +NGHTTP3_EXTERN int nghttp3_conn_close_wt_session(nghttp3_conn *conn, + int64_t session_id, + uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen); + +/** + * @function + * + * `nghttp3_conn_get_stream_wt_session_id` returns the WebTransport + * session ID of a stream denoted by |stream_id| if it is WebTransport + * data stream. If the stream is not found, it is not a WebTransport + * data stream, or it is unable to get session ID, this function + * returns -1. + */ +NGHTTP3_EXTERN int64_t nghttp3_conn_get_stream_wt_session_id( + const nghttp3_conn *conn, int64_t stream_id); + /** * @function * diff --git a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h index adb41c1c7b2d..bbe445ae0f44 100644 --- a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h +++ b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h @@ -31,7 +31,7 @@ * * Version number of the nghttp3 library release. */ -#define NGHTTP3_VERSION "1.18.0" +#define NGHTTP3_VERSION "1.18.0-DEV" /** * @macro diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c index 04b50ac3b632..ad007fc2f8a4 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c @@ -36,9 +36,22 @@ #include "nghttp3_unreachable.h" #include "nghttp3_settings.h" #include "nghttp3_callbacks.h" +#include "nghttp3_wt.h" nghttp3_objalloc_def(chunk, nghttp3_chunk, oplent) +/* + * conn_remote_stream returns nonzero if |stream_id| is a remote + * stream ID. + */ +static int conn_remote_stream(const nghttp3_conn *conn, int64_t stream_id) { + if (conn->server) { + return !(stream_id & 0x1); + } + + return stream_id & 0x1; +} + /* * conn_remote_stream_uni returns nonzero if |stream_id| is remote * unidirectional stream ID. @@ -50,6 +63,30 @@ static int conn_remote_stream_uni(const nghttp3_conn *conn, int64_t stream_id) { return (stream_id & 0x03) == 0x03; } +static int conn_wt_enabled(const nghttp3_conn *conn) { + const nghttp3_settings *local_settings = &conn->local.settings; + const nghttp3_proto_settings *remote_settings = &conn->remote.settings; + + if (!local_settings->wt_enabled || !local_settings->h3_datagram) { + return 0; + } + + if (conn->server) { + return (!(conn->flags & NGHTTP3_CONN_FLAG_SETTINGS_RECVED) || + /* TODO client sends SETTINGS_WT_ENABLED for draft + versions only. But some client implementations do not + send it. For interop purpose, do not require this + remote setting for now. */ + (/* remote_settings->wt_enabled && */ remote_settings + ->h3_datagram)) && + local_settings->enable_connect_protocol; + } + + return remote_settings->wt_enabled && + remote_settings->enable_connect_protocol && + remote_settings->h3_datagram; +} + static int conn_call_begin_headers(nghttp3_conn *conn, nghttp3_stream *stream) { int rv; @@ -251,6 +288,70 @@ static int conn_call_end_origin(nghttp3_conn *conn) { return 0; } +static int conn_call_recv_data(nghttp3_conn *conn, const nghttp3_stream *stream, + const uint8_t *data, size_t datalen) { + int rv; + + if (!conn->callbacks.recv_data) { + return 0; + } + + rv = conn->callbacks.recv_data(conn, stream->node.id, data, datalen, + conn->user_data, stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int conn_call_recv_wt_data(nghttp3_conn *conn, + const nghttp3_stream *stream, + const uint8_t *data, size_t datalen) { + nghttp3_wt_session *wt_session; + int rv; + + if (!conn->callbacks.recv_wt_data) { + return 0; + } + + wt_session = stream->wt.session; + + assert(wt_session); + + rv = conn->callbacks.recv_wt_data(conn, wt_session->session_id, + stream->node.id, data, datalen, + conn->user_data, stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} + +static int conn_call_wt_data_stream_open(nghttp3_conn *conn, + const nghttp3_stream *stream) { + nghttp3_wt_session *wt_session; + int rv; + + if (!conn->callbacks.wt_data_stream_open) { + return 0; + } + + wt_session = stream->wt.session; + + assert(wt_session); + + rv = conn->callbacks.wt_data_stream_open(conn, wt_session->session_id, + stream->node.id, conn->user_data, + stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} + static int conn_glitch_ratelim_drain(nghttp3_conn *conn, uint64_t n, nghttp3_tstamp ts) { if (ts == UINT64_MAX) { @@ -401,11 +502,25 @@ int nghttp3_conn_server_new_versioned(nghttp3_conn **pconn, return 0; } +static void remove_wt_session_ref(nghttp3_wt_session *wt_session) { + nghttp3_stream *stream; + + for (stream = wt_session->head; stream; stream = stream->wt.next) { + assert(stream->wt.session == wt_session); + + stream->wt.session = NULL; + } +} + static int free_stream(void *data, void *ptr) { nghttp3_stream *stream = data; (void)ptr; + if (nghttp3_stream_wt_ctrl(stream)) { + remove_wt_session_ref(stream->wt.session); + } + nghttp3_stream_del(stream); return 0; @@ -501,6 +616,10 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, return rv; } + if (conn_wt_enabled(conn)) { + stream->flags |= NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA; + } + if ((conn->flags & NGHTTP3_CONN_FLAG_GOAWAY_QUEUED) && conn->tx.goaway_id <= stream_id) { stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; @@ -527,7 +646,9 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, } stream->rx.hstate = NGHTTP3_HTTP_STATE_REQ_INITIAL; - } else if (nghttp3_server_stream_uni(stream_id)) { + } else if (nghttp3_server_stream_uni(stream_id) || + (conn_wt_enabled(conn) && + nghttp3_server_stream_bidi(stream_id))) { if (srclen == 0 && fin) { return 0; } @@ -537,6 +658,10 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, return rv; } + if (!(stream_id & 0x2) && conn_wt_enabled(conn)) { + stream->flags |= NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA; + } + stream->rx.hstate = NGHTTP3_HTTP_STATE_RESP_INITIAL; } else { /* client doesn't expect to receive new bidirectional stream or @@ -545,10 +670,12 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, } } else if (conn->server) { assert(nghttp3_client_stream_bidi(stream_id) || - nghttp3_client_stream_uni(stream_id)); + nghttp3_client_stream_uni(stream_id) || + (conn_wt_enabled(conn) && nghttp3_server_stream_bidi(stream_id))); } else { assert(nghttp3_client_stream_bidi(stream_id) || - nghttp3_server_stream_uni(stream_id)); + nghttp3_server_stream_uni(stream_id) || + (conn_wt_enabled(conn) && nghttp3_server_stream_bidi(stream_id))); } if (srclen == 0 && !fin) { @@ -613,6 +740,12 @@ static nghttp3_ssize conn_read_type(nghttp3_conn *conn, nghttp3_stream *stream, conn->flags |= NGHTTP3_CONN_FLAG_QPACK_DECODER_OPENED; stream->type = NGHTTP3_STREAM_TYPE_QPACK_DECODER; break; + case NGHTTP3_STREAM_TYPE_WT_STREAM: + if (!conn_wt_enabled(conn)) { + return NGHTTP3_ERR_H3_STREAM_CREATION_ERROR; + } + stream->type = NGHTTP3_STREAM_TYPE_WT_STREAM; + break; default: stream->type = NGHTTP3_STREAM_TYPE_UNKNOWN; break; @@ -705,6 +838,10 @@ nghttp3_ssize nghttp3_conn_read_uni(nghttp3_conn *conn, nghttp3_stream *stream, } nconsumed = nghttp3_conn_read_qpack_decoder(conn, src, srclen); break; + case NGHTTP3_STREAM_TYPE_WT_STREAM: + nconsumed = + nghttp3_conn_read_wt_stream_uni(conn, stream, src, srclen, fin, ts); + break; case NGHTTP3_STREAM_TYPE_UNKNOWN: nconsumed = (nghttp3_ssize)srclen; break; @@ -797,7 +934,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, case NGHTTP3_FRAME_SETTINGS: /* SETTINGS frame might be empty. */ if (rstate->left == 0) { - rv = conn_call_recv_settings(conn); + rv = nghttp3_conn_on_settings_received(conn); if (rv != 0) { return rv; } @@ -895,7 +1032,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, case NGHTTP3_CTRL_STREAM_STATE_SETTINGS: for (;;) { if (rstate->left == 0) { - rv = conn_call_recv_settings(conn); + rv = nghttp3_conn_on_settings_received(conn); if (rv != 0) { return rv; } @@ -1013,7 +1150,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, break; } - rv = conn_call_recv_settings(conn); + rv = nghttp3_conn_on_settings_received(conn); if (rv != 0) { return rv; } @@ -1203,7 +1340,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, for (; conn->rx.originlen_offset < sizeof(conn->rx.originlen) && (size_t)nread < len; ++conn->rx.originlen_offset, ++nread) { - conn->rx.originlen *= 256; + conn->rx.originlen <<= 8; conn->rx.originlen += *p++; } @@ -1338,6 +1475,35 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, return (nghttp3_ssize)nconsumed; } +static int conn_unlink_wt_session(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream) { + nghttp3_wt_session *wt_session = wt_ctrl_stream->wt.session; + nghttp3_stream *stream, *next; + int rv; + (void)rv; + + for (stream = wt_session->head; stream;) { + next = stream->wt.next; + + assert(stream->wt.session); + + stream->wt.session = NULL; + stream->wt.prev = stream->wt.next = NULL; + + rv = nghttp3_conn_shutdown_wt_data_stream(conn, stream, + NGHTTP3_WT_SESSION_GONE); + if (rv != 0) { + return rv; + } + + stream = next; + } + + wt_session->head = NULL; + + return 0; +} + static int conn_delete_stream(nghttp3_conn *conn, nghttp3_stream *stream, uint32_t flags, uint64_t rx_app_error_code, uint64_t tx_app_error_code) { @@ -1385,6 +1551,15 @@ static int conn_delete_stream(nghttp3_conn *conn, nghttp3_stream *stream, } } + if (nghttp3_stream_wt_ctrl(stream)) { + rv = conn_unlink_wt_session(conn, stream); + if (rv != 0) { + return rv; + } + } else if (nghttp3_stream_wt_data(stream)) { + nghttp3_wt_session_remove_stream(stream->wt.session, stream); + } + if (conn->server && nghttp3_client_stream_bidi(stream->node.id)) { assert(conn->remote.bidi.num_streams > 0); @@ -1518,6 +1693,7 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, int rv; nghttp3_stream_read_state *rstate = &stream->rstate; nghttp3_varint_read_state *rvint = &rstate->rvint; + nghttp3_stream *wt_ctrl_stream; nghttp3_ssize nread; size_t nconsumed = 0; int busy = 0; @@ -1529,7 +1705,8 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, return (nghttp3_ssize)srclen; } - if (stream->flags & NGHTTP3_STREAM_FLAG_QPACK_DECODE_BLOCKED) { + if (stream->flags & (NGHTTP3_STREAM_FLAG_QPACK_DECODE_BLOCKED | + NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED)) { *pnproc = 0; if (srclen == 0) { @@ -1638,6 +1815,42 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, } rstate->state = NGHTTP3_REQ_STREAM_STATE_HEADERS; + break; + case NGHTTP3_EXFR_WT_STREAM_BIDI: + if (!nghttp3_stream_wt_data(stream) && + !(stream->flags & NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA)) { + return NGHTTP3_ERR_H3_FRAME_ERROR; + } + + stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA; + + if (conn->server) { + if (stream->rx.hstate != NGHTTP3_HTTP_STATE_REQ_INITIAL) { + return NGHTTP3_ERR_H3_FRAME_ERROR; + } + } else if (stream->rx.hstate != NGHTTP3_HTTP_STATE_RESP_INITIAL) { + return NGHTTP3_ERR_H3_FRAME_ERROR; + } + + /* rstate->left is Session ID */ + rv = nghttp3_conn_on_wt_stream(conn, stream, (int64_t)rstate->left); + if (rv != 0) { + if (rv != NGHTTP3_ERR_WT_SESSION_GONE) { + return rv; + } + + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + rv = nghttp3_conn_abort_stream(conn, stream, NGHTTP3_WT_SESSION_GONE); + if (rv != 0) { + return rv; + } + + break; + } + + rstate->state = NGHTTP3_REQ_STREAM_STATE_BEFORE_WT_DATA; + break; case NGHTTP3_FRAME_PUSH_PROMISE: /* We do not support push */ case NGHTTP3_FRAME_CANCEL_PUSH: @@ -1657,6 +1870,8 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, return NGHTTP3_ERR_H3_EXCESSIVE_LOAD; } + stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA; + /* TODO Handle reserved frame type */ busy = 1; rstate->state = NGHTTP3_REQ_STREAM_STATE_IGN_FRAME; @@ -1665,11 +1880,29 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, break; case NGHTTP3_REQ_STREAM_STATE_DATA: len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); - rv = nghttp3_conn_on_data(conn, stream, p, len); - if (rv != 0) { - return rv; + nread = nghttp3_conn_on_data(conn, stream, p, len); + if (nread < 0) { + if (nread != NGHTTP3_ERR_WT_SESSION_GONE) { + return nread; + } + + rv = nghttp3_conn_shutdown_wt_session(conn, stream, + NGHTTP3_WT_SESSION_GONE); + if (rv != 0) { + return rv; + } + + /* Now that the stream is in + NGHTTP3_REQ_STREAM_STATE_IGN_REST, end_stream callback is + not called. */ + + /* Pretend that all stream data have been consumed */ + nconsumed += len; + + goto almost_done; } p += len; + nconsumed += (size_t)nread; rstate->left -= len; if (rstate->left) { @@ -1741,7 +1974,9 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, return rv; } } - /* fall through */ + + rv = conn_call_end_headers(conn, stream, p == end && fin); + break; case NGHTTP3_HTTP_STATE_RESP_HEADERS_BEGIN: rv = conn_call_end_headers(conn, stream, p == end && fin); break; @@ -1763,6 +1998,62 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, nghttp3_stream_read_state_reset(rstate); + if (conn->server) { + if (stream->rx.hstate == NGHTTP3_HTTP_STATE_REQ_HEADERS_END) { + if (stream->wt.session && (stream->wt.session->flags & + NGHTTP3_WT_SESSION_FLAG_RESP_SUBMITTED)) { + /* Server has submitted WebTransport session. */ + rv = nghttp3_conn_on_wt_session_confirmed(conn, stream, ts); + if (rv != 0) { + return rv; + } + } else if (stream->rx.http.flags & NGHTTP3_HTTP_FLAG_WEBTRANSPORT) { + if (stream->flags & NGHTTP3_STREAM_FLAG_RESP_SUBMITTED) { + /* Server refused WebTransport upgrade request */ + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + rv = conn_call_stop_sending(conn, stream, NGHTTP3_H3_NO_ERROR); + if (rv != 0) { + return rv; + } + } else { + /* Server has not submitted response */ + stream->flags |= NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED; + if (p != end) { + rv = nghttp3_stream_buffer_data(stream, p, (size_t)(end - p)); + if (rv != 0) { + return rv; + } + } + + *pnproc = (size_t)(p - src); + + return (nghttp3_ssize)nconsumed; + } + } + } + } else if (stream->rx.hstate == NGHTTP3_HTTP_STATE_RESP_HEADERS_END && + stream->wt.session) { + if (stream->rx.http.status_code / 100 == 2) { + rv = nghttp3_conn_on_wt_session_confirmed(conn, stream, ts); + if (rv != 0) { + return rv; + } + } else { + /* Server refused WebTransport negotiation. Reset the session + stream. This could be a redirect, but client is instructed + not to follow the redirect automatically. Most of the + case, we cannot do anything but just close the stream. */ + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + rv = nghttp3_conn_abort_stream(conn, stream, + NGHTTP3_H3_REQUEST_CANCELLED); + if (rv != 0) { + return rv; + } + } + } + break; case NGHTTP3_REQ_STREAM_STATE_IGN_FRAME: len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); @@ -1776,6 +2067,40 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, nghttp3_stream_read_state_reset(rstate); break; + case NGHTTP3_REQ_STREAM_STATE_BEFORE_WT_DATA: + rstate->state = NGHTTP3_REQ_STREAM_STATE_WT_DATA; + + assert(stream->wt.session); + + wt_ctrl_stream = + nghttp3_conn_find_stream(conn, stream->wt.session->session_id); + + if (!(wt_ctrl_stream->wt.session->flags & + NGHTTP3_WT_SESSION_FLAG_CONFIRMED)) { + stream->flags |= NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED; + + if (p != end) { + rv = nghttp3_stream_buffer_data(stream, p, (size_t)(end - p)); + if (rv != 0) { + return rv; + } + } + + *pnproc = (size_t)(p - src); + + return (nghttp3_ssize)nconsumed; + } + + break; + case NGHTTP3_REQ_STREAM_STATE_WT_DATA: + rv = conn_call_recv_wt_data(conn, stream, p, (size_t)(end - p)); + if (rv != 0) { + return rv; + } + + p = end; + + goto almost_done; case NGHTTP3_REQ_STREAM_STATE_IGN_REST: nconsumed += (size_t)(end - p); *pnproc = (size_t)(end - src); @@ -1795,10 +2120,16 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, if (rv != 0) { return rv; } + + /* Fall through */ + /* When a stream is closed without any data */ + case NGHTTP3_REQ_STREAM_STATE_BEFORE_WT_DATA: + case NGHTTP3_REQ_STREAM_STATE_WT_DATA: rv = conn_call_end_stream(conn, stream); if (rv != 0) { return rv; } + break; case NGHTTP3_REQ_STREAM_STATE_IGN_REST: break; @@ -1811,8 +2142,8 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, return (nghttp3_ssize)nconsumed; } -int nghttp3_conn_on_data(nghttp3_conn *conn, nghttp3_stream *stream, - const uint8_t *data, size_t datalen) { +nghttp3_ssize nghttp3_conn_on_data(nghttp3_conn *conn, nghttp3_stream *stream, + const uint8_t *data, size_t datalen) { int rv; rv = nghttp3_http_on_data_chunk(stream, datalen); @@ -1820,17 +2151,21 @@ int nghttp3_conn_on_data(nghttp3_conn *conn, nghttp3_stream *stream, return rv; } - if (!conn->callbacks.recv_data) { - return 0; + if (!stream->wt.session) { + return conn_call_recv_data(conn, stream, data, datalen); } - rv = conn->callbacks.recv_data(conn, stream->node.id, data, datalen, - conn->user_data, stream->user_data); + /* The stream data must be buffered until WebTransport session has + been confirmed. */ + assert(stream->wt.session->flags & NGHTTP3_WT_SESSION_FLAG_CONFIRMED); + + rv = nghttp3_conn_read_wt_ctrl_stream(conn, stream, data, datalen); if (rv != 0) { - return NGHTTP3_ERR_CALLBACK_FAILURE; + return rv; } - return 0; + /* WebTransport control stream has consumed all data */ + return (nghttp3_ssize)datalen; } static nghttp3_pq *conn_get_sched_pq(nghttp3_conn *conn, nghttp3_tnode *tnode) { @@ -2025,6 +2360,14 @@ int nghttp3_conn_on_settings_entry_received(nghttp3_conn *conn, dest->h3_datagram = (uint8_t)ent->value; break; + case NGHTTP3_SETTINGS_ID_WT_ENABLED: + /* compat for pre draft-15 */ + case NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS: + case NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS_DRAFT7: + /* compat for ancient draft */ + case NGHTTP3_SETTINGS_ID_ENABLE_WEBTRANSPORT_DRAFT2: + dest->wt_enabled = ent->value != 0; + break; case NGHTTP3_H2_SETTINGS_ID_ENABLE_PUSH: case NGHTTP3_H2_SETTINGS_ID_MAX_CONCURRENT_STREAMS: case NGHTTP3_H2_SETTINGS_ID_INITIAL_WINDOW_SIZE: @@ -2038,6 +2381,37 @@ int nghttp3_conn_on_settings_entry_received(nghttp3_conn *conn, return 0; } +static int abort_wt_session(void *data, void *ptr) { + nghttp3_conn *conn = ptr; + nghttp3_stream *stream = data; + + if (!nghttp3_stream_wt_ctrl(stream)) { + return 0; + } + + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + return nghttp3_conn_abort_stream(conn, stream, + NGHTTP3_H3_GENERAL_PROTOCOL_ERROR); +} + +int nghttp3_conn_on_settings_received(nghttp3_conn *conn) { + int rv; + + conn->flags |= NGHTTP3_CONN_FLAG_SETTINGS_RECVED; + + rv = conn_call_recv_settings(conn); + if (rv != 0) { + return rv; + } + + if (!conn->local.settings.wt_enabled || conn_wt_enabled(conn)) { + return 0; + } + + return nghttp3_map_each(&conn->streams, abort_wt_session, conn); +} + static int conn_on_priority_update_stream(nghttp3_conn *conn, const nghttp3_frame_priority_update *fr) { @@ -2079,6 +2453,11 @@ conn_on_priority_update_stream(nghttp3_conn *conn, stream->node.pri = fr->pri; stream->flags |= NGHTTP3_STREAM_FLAG_PRIORITY_UPDATE_RECVED; + + if (conn_wt_enabled(conn)) { + stream->flags |= NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA; + } + stream->rx.hstate = NGHTTP3_HTTP_STATE_REQ_INITIAL; return 0; @@ -2347,7 +2726,7 @@ nghttp3_ssize nghttp3_conn_writev_stream(nghttp3_conn *conn, return ncnt; } - if (nghttp3_client_stream_bidi(stream->node.id) && + if (nghttp3_stream_schedulable(stream) && !nghttp3_stream_require_schedule(stream)) { nghttp3_conn_unschedule_stream(conn, stream); } @@ -2386,7 +2765,7 @@ int nghttp3_conn_add_write_offset(nghttp3_conn *conn, int64_t stream_id, stream->unscheduled_nwrite += n; - if (!nghttp3_client_stream_bidi(stream->node.id)) { + if (!nghttp3_stream_schedulable(stream)) { return 0; } @@ -2428,6 +2807,22 @@ int nghttp3_conn_update_ack_offset(nghttp3_conn *conn, int64_t stream_id, return nghttp3_stream_update_ack_offset(stream, offset); } +static nghttp3_ssize wt_session_read_data(nghttp3_conn *conn, int64_t stream_id, + nghttp3_vec *vec, size_t veccnt, + uint32_t *pflags, + void *conn_user_data, + void *stream_user_data) { + (void)conn; + (void)stream_id; + (void)vec; + (void)veccnt; + (void)pflags; + (void)conn_user_data; + (void)stream_user_data; + + return NGHTTP3_ERR_WOULDBLOCK; +} + static int conn_submit_headers_data(nghttp3_conn *conn, nghttp3_stream *stream, const nghttp3_nv *nva, size_t nvlen, const nghttp3_data_reader *dr) { @@ -2452,7 +2847,7 @@ static int conn_submit_headers_data(nghttp3_conn *conn, nghttp3_stream *stream, .nvlen = nvlen, }; - if (dr) { + if (dr && dr->read_data != wt_session_read_data) { rv = nghttp3_stream_frq_emplace(stream, &fr); if (rv != 0) { return rv; @@ -2579,6 +2974,8 @@ int nghttp3_conn_submit_response(nghttp3_conn *conn, int64_t stream_id, stream->flags |= NGHTTP3_STREAM_FLAG_WRITE_END_STREAM; } + stream->flags |= NGHTTP3_STREAM_FLAG_RESP_SUBMITTED; + return conn_submit_headers_data(conn, stream, nva, nvlen, dr); } @@ -2656,14 +3053,27 @@ int nghttp3_conn_shutdown(nghttp3_conn *conn) { } int nghttp3_conn_reject_stream(nghttp3_conn *conn, nghttp3_stream *stream) { + return nghttp3_conn_abort_stream(conn, stream, NGHTTP3_H3_REQUEST_REJECTED); +} + +int nghttp3_conn_abort_stream(nghttp3_conn *conn, nghttp3_stream *stream, + uint64_t error_code) { int rv; + int remote_uni = conn_remote_stream_uni(conn, stream->node.id); + int bidi = !nghttp3_stream_uni(stream->node.id); - rv = conn_call_stop_sending(conn, stream, NGHTTP3_H3_REQUEST_REJECTED); - if (rv != 0) { - return rv; + if (remote_uni || bidi) { + rv = conn_call_stop_sending(conn, stream, error_code); + if (rv != 0) { + return rv; + } + } + + if (remote_uni) { + return 0; } - return conn_call_reset_stream(conn, stream, NGHTTP3_H3_REQUEST_REJECTED); + return conn_call_reset_stream(conn, stream, error_code); } void nghttp3_conn_block_stream(nghttp3_conn *conn, int64_t stream_id) { @@ -2705,7 +3115,7 @@ int nghttp3_conn_unblock_stream(nghttp3_conn *conn, int64_t stream_id) { stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_FC_BLOCKED; - if (nghttp3_client_stream_bidi(stream->node.id) && + if (nghttp3_stream_schedulable(stream) && nghttp3_stream_require_schedule(stream)) { return nghttp3_conn_ensure_stream_scheduled(conn, stream); } @@ -2739,7 +3149,7 @@ int nghttp3_conn_resume_stream(nghttp3_conn *conn, int64_t stream_id) { stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED; - if (nghttp3_client_stream_bidi(stream->node.id) && + if (nghttp3_stream_schedulable(stream) && nghttp3_stream_require_schedule(stream)) { return nghttp3_conn_ensure_stream_scheduled(conn, stream); } @@ -2765,8 +3175,7 @@ int nghttp3_conn_close_stream2(nghttp3_conn *conn, uint32_t flags, return NGHTTP3_ERR_STREAM_NOT_FOUND; } - if (nghttp3_stream_uni(stream_id) && - stream->type != NGHTTP3_STREAM_TYPE_UNKNOWN) { + if (nghttp3_stream_critical(stream)) { return NGHTTP3_ERR_H3_CLOSED_CRITICAL_STREAM; } @@ -2793,6 +3202,13 @@ int nghttp3_conn_shutdown_stream_read(nghttp3_conn *conn, int64_t stream_id) { } stream->flags |= NGHTTP3_STREAM_FLAG_SHUT_RD; + + /* If stream is WebTransport data stream, do not send QPACK Stream + Cancellation. */ + if (nghttp3_stream_wt_data(stream) || + (stream->flags & NGHTTP3_STREAM_FLAG_WT_DATA)) { + return 0; + } } return nghttp3_qpack_decoder_cancel_stream(&conn->qdec, stream_id); @@ -3032,5 +3448,789 @@ int nghttp3_conn_is_stream_flushed(const nghttp3_conn *conn, fr = nghttp3_ringbuf_get(&stream->frq, 0); - return fr->hd.type == NGHTTP3_FRAME_DATA; + return fr->hd.type == NGHTTP3_FRAME_DATA || + (fr->hd.type == NGHTTP3_FRAME_EX_WT && + fr->wt.fr.hd.type == NGHTTP3_EXFR_WT_STREAM_DATA); +} + +int nghttp3_conn_submit_wt_request(nghttp3_conn *conn, int64_t stream_id, + const nghttp3_nv *nva, size_t nvlen, + void *stream_user_data) { + int rv; + nghttp3_stream *stream; + + if (!conn_wt_enabled(conn)) { + return NGHTTP3_ERR_INVALID_STATE; + } + + rv = nghttp3_conn_submit_request( + conn, stream_id, nva, nvlen, + &(nghttp3_data_reader){.read_data = wt_session_read_data}, + stream_user_data); + if (rv != 0) { + return rv; + } + + stream = nghttp3_conn_find_stream(conn, stream_id); + + assert(stream); + + return nghttp3_conn_open_wt_session(conn, stream); +} + +int nghttp3_conn_submit_wt_response(nghttp3_conn *conn, int64_t stream_id, + const nghttp3_nv *nva, size_t nvlen) { + int rv; + nghttp3_stream *stream; + + if (!conn_wt_enabled(conn)) { + return NGHTTP3_ERR_INVALID_STATE; + } + + rv = nghttp3_conn_submit_response( + conn, stream_id, nva, nvlen, + &(nghttp3_data_reader){.read_data = wt_session_read_data}); + if (rv != 0) { + return rv; + } + + stream = nghttp3_conn_find_stream(conn, stream_id); + + if (!stream->wt.session) { + rv = nghttp3_conn_open_wt_session(conn, stream); + if (rv != 0) { + return rv; + } + } + + stream->wt.session->flags |= NGHTTP3_WT_SESSION_FLAG_RESP_SUBMITTED; + + return 0; +} + +int nghttp3_conn_server_confirm_wt_session(nghttp3_conn *conn, + int64_t session_id, + nghttp3_tstamp ts) { + nghttp3_stream *wt_ctrl_stream; + + wt_ctrl_stream = nghttp3_conn_find_stream(conn, session_id); + if (!wt_ctrl_stream) { + return NGHTTP3_ERR_STREAM_NOT_FOUND; + } + + assert(wt_ctrl_stream->wt.session); + assert(wt_ctrl_stream->wt.session->flags & + NGHTTP3_WT_SESSION_FLAG_RESP_SUBMITTED); + + wt_ctrl_stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED; + + return nghttp3_conn_on_wt_session_confirmed(conn, wt_ctrl_stream, ts); +} + +int nghttp3_conn_open_wt_session(nghttp3_conn *conn, nghttp3_stream *stream) { + int rv; + + rv = nghttp3_wt_session_new(&stream->wt.session, stream->node.id, conn->mem); + if (rv != 0) { + return rv; + } + + return 0; +} + +int nghttp3_conn_open_wt_data_stream(nghttp3_conn *conn, int64_t session_id, + int64_t stream_id, + const nghttp3_data_reader *dr, + void *stream_user_data) { + nghttp3_stream *stream, *wt_ctrl_stream; + nghttp3_wt_session *wt_session; + nghttp3_frame *fr; + uint64_t type; + int rv; + int remote_bidi = 0; + + if (conn->server) { + assert(nghttp3_client_stream_bidi(stream_id) || + nghttp3_server_stream_bidi(stream_id) || + nghttp3_server_stream_uni(stream_id)); + } else { + assert(nghttp3_client_stream_bidi(stream_id) || + nghttp3_server_stream_bidi(stream_id) || + nghttp3_client_stream_uni(stream_id)); + } + + /* TODO Check session flow control */ + + assert(dr); + + if (conn->flags & NGHTTP3_CONN_FLAG_GOAWAY_RECVED) { + return NGHTTP3_ERR_CONN_CLOSING; + } + + wt_ctrl_stream = nghttp3_conn_find_stream(conn, session_id); + if (!wt_ctrl_stream || !wt_ctrl_stream->wt.session) { + return NGHTTP3_ERR_INVALID_ARGUMENT; + } + + wt_session = wt_ctrl_stream->wt.session; + + stream = nghttp3_conn_find_stream(conn, stream_id); + if (stream) { + if (conn->server) { + assert(nghttp3_client_stream_bidi(stream_id)); + } else { + assert(nghttp3_server_stream_bidi(stream_id)); + } + + /* TODO verify that we do not start writing more than once. */ + + /* Normally, stream->wt.session is not NULL because we must + identify WT stream header first. The only exception is a + stream create by priority update on server side. But it must + be client initiated bidi stream, and we must wait for its WT + header. */ + if (!stream->wt.session) { + return NGHTTP3_ERR_INVALID_ARGUMENT; + } + + if (stream->flags & NGHTTP3_STREAM_FLAG_WRITE_END_STREAM) { + return NGHTTP3_ERR_INVALID_STATE; + } + + remote_bidi = 1; + + if (stream_user_data) { + stream->user_data = stream_user_data; + } + + if (conn->server) { + stream->flags |= NGHTTP3_STREAM_FLAG_SERVER_PRIORITY_SET; + } + + assert(!nghttp3_tnode_is_scheduled(&stream->node)); + } else { + if (conn->server) { + assert(nghttp3_server_stream_bidi(stream_id) || + nghttp3_server_stream_uni(stream_id)); + } else { + assert(nghttp3_client_stream_bidi(stream_id) || + nghttp3_client_stream_uni(stream_id)); + } + + rv = nghttp3_conn_create_stream(conn, &stream, stream_id); + if (rv != 0) { + return rv; + } + + nghttp3_wt_session_add_stream(wt_session, stream); + + if (conn->server) { + stream->rx.hstate = NGHTTP3_HTTP_STATE_REQ_INITIAL; + } else { + stream->rx.hstate = NGHTTP3_HTTP_STATE_RESP_INITIAL; + } + + stream->user_data = stream_user_data; + + if (stream_id & 0x2) { + stream->flags |= NGHTTP3_STREAM_FLAG_SHUT_RD; + stream->type = NGHTTP3_STREAM_TYPE_WT_STREAM; + } + } + + stream->node.pri = (nghttp3_pri){ + .urgency = NGHTTP3_DEFAULT_URGENCY, + .inc = 1, + }; + + if (stream_id & 0x2) { + type = NGHTTP3_EXFR_WT_STREAM_UNI; + } else if (remote_bidi) { + type = NGHTTP3_EXFR_WT_STREAM_DATA; + } else { + type = NGHTTP3_EXFR_WT_STREAM_BIDI; + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_BEFORE_WT_DATA; + } + + rv = nghttp3_stream_frq_emplace(stream, &fr); + if (rv != 0) { + return rv; + } + + fr->wt = (nghttp3_frame_ex_wt){ + .type = NGHTTP3_FRAME_EX_WT, + .fr.wt_stream = + { + .type = type, + .session_id = session_id, + .dr = *dr, + }, + }; + + if (nghttp3_stream_require_schedule(stream)) { + return nghttp3_conn_ensure_stream_scheduled(conn, stream); + } + + return 0; +} + +int nghttp3_conn_close_wt_session(nghttp3_conn *conn, int64_t session_id, + uint32_t wt_error_code, const uint8_t *msg, + size_t msglen) { + nghttp3_stream *stream; + nghttp3_wt_session *wt_session; + nghttp3_frame *fr; + int rv; + + stream = nghttp3_conn_find_stream(conn, session_id); + if (stream == NULL) { + return NGHTTP3_ERR_STREAM_NOT_FOUND; + } + + if (!nghttp3_stream_wt_ctrl(stream)) { + return NGHTTP3_ERR_INVALID_ARGUMENT; + } + + if (stream->flags & NGHTTP3_STREAM_FLAG_WRITE_END_STREAM) { + return NGHTTP3_ERR_INVALID_STATE; + } + + stream->flags |= NGHTTP3_STREAM_FLAG_WRITE_END_STREAM; + + wt_session = stream->wt.session; + + assert(!wt_session->tx.error_msg.base); + + if (msglen) { + wt_session->tx.error_msg.base = nghttp3_mem_malloc(conn->mem, msglen); + if (!wt_session->tx.error_msg.base) { + return NGHTTP3_ERR_NOMEM; + } + + memcpy(wt_session->tx.error_msg.base, msg, msglen); + wt_session->tx.error_msg.len = msglen; + } + + rv = nghttp3_stream_frq_emplace(stream, &fr); + if (rv != 0) { + return rv; + } + + fr->cpsl = (nghttp3_frame_ex_cpsl){ + .type = NGHTTP3_FRAME_EX_CPSL, + .fr.wt_close_session = + { + .type = NGHTTP3_EXFR_CPSL_WT_CLOSE_SESSION, + .error_code = wt_error_code, + .error_msg = wt_session->tx.error_msg, + }, + }; + + if (nghttp3_stream_require_schedule(stream)) { + rv = nghttp3_conn_schedule_stream(conn, stream); + if (rv != 0) { + return rv; + } + } + + rv = nghttp3_conn_shutdown_all_wt_data_streams(conn, stream, + NGHTTP3_WT_SESSION_GONE); + if (rv != 0) { + return rv; + } + + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + return conn_call_stop_sending(conn, stream, NGHTTP3_WT_SESSION_GONE); +} + +int nghttp3_conn_on_wt_stream(nghttp3_conn *conn, nghttp3_stream *stream, + int64_t session_id) { + nghttp3_stream *wt_ctrl_stream; + nghttp3_wt_session *wt_session; + int rv; + + if (!nghttp3_client_stream_bidi(session_id)) { + return NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; + } + + if (stream->wt.session) { + assert(stream->wt.session->session_id != stream->node.id); + + if (stream->wt.session->session_id != session_id) { + return NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; + } + + return 0; + } + + wt_ctrl_stream = nghttp3_conn_find_stream(conn, session_id); + if (!wt_ctrl_stream) { + if (!conn->server) { + /* On client's perspective, if session stream is not found, we are + sure that session is gone. */ + return NGHTTP3_ERR_WT_SESSION_GONE; + } + + if (nghttp3_ord_stream_id(session_id) > + conn->remote.bidi.max_client_streams) { + return NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; + } + + if ((conn->flags & NGHTTP3_CONN_FLAG_GOAWAY_QUEUED) && + conn->tx.goaway_id <= session_id) { + return NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; + } + + rv = conn_bidi_idtr_open(conn, session_id); + if (rv != 0) { + if (nghttp3_err_is_fatal(rv)) { + return rv; + } + + return NGHTTP3_ERR_WT_SESSION_GONE; + } + + conn->rx.max_stream_id_bidi = + nghttp3_max(conn->rx.max_stream_id_bidi, session_id); + rv = nghttp3_conn_create_stream(conn, &wt_ctrl_stream, session_id); + if (rv != 0) { + return rv; + } + + wt_ctrl_stream->rx.hstate = NGHTTP3_HTTP_STATE_REQ_INITIAL; + } + + if (!wt_ctrl_stream->wt.session) { + rv = nghttp3_conn_open_wt_session(conn, wt_ctrl_stream); + if (rv != 0) { + return rv; + } + } + + wt_session = wt_ctrl_stream->wt.session; + + assert(wt_session); + + nghttp3_wt_session_add_stream(wt_session, stream); + + if (wt_ctrl_stream->wt.session->flags & NGHTTP3_WT_SESSION_FLAG_CONFIRMED) { + return conn_call_wt_data_stream_open(conn, stream); + } + + return 0; +} + +int nghttp3_conn_on_wt_session_confirmed(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + nghttp3_tstamp ts) { + nghttp3_stream *stream; + nghttp3_wt_session *wt_session = wt_ctrl_stream->wt.session; + int rv; + + wt_session->flags |= NGHTTP3_WT_SESSION_FLAG_CONFIRMED; + + /* TODO Is stream gone during iteration? */ + for (stream = wt_session->head; stream; stream = stream->wt.next) { + stream->flags &= (uint16_t)~NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED; + + if (conn_remote_stream(conn, stream->node.id)) { + rv = conn_call_wt_data_stream_open(conn, stream); + if (rv != 0) { + return rv; + } + } + + rv = nghttp3_conn_process_blocked_wt_stream_data(conn, stream, ts); + if (rv != 0) { + return rv; + } + } + + return nghttp3_conn_process_blocked_wt_stream_data(conn, wt_ctrl_stream, ts); +} + +nghttp3_ssize nghttp3_conn_read_wt_stream_uni(nghttp3_conn *conn, + nghttp3_stream *stream, + const uint8_t *src, size_t srclen, + int fin, nghttp3_tstamp ts) { + const uint8_t *p = src, *end = src ? src + srclen : src; + int rv; + nghttp3_stream_read_state *rstate = &stream->rstate; + nghttp3_varint_read_state *rvint = &rstate->rvint; + nghttp3_ssize nread; + size_t nconsumed = 0; + nghttp3_stream *wt_ctrl_stream; + (void)ts; + + if ((stream->flags & NGHTTP3_STREAM_FLAG_SHUT_RD)) { + return (nghttp3_ssize)srclen; + } + + if (srclen == 0) { + goto almost_done; + } + + if (stream->flags & NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED) { + if (srclen == 0) { + return 0; + } + + rv = nghttp3_stream_buffer_data(stream, p, srclen); + if (rv != 0) { + return rv; + } + + return 0; + } + + switch (rstate->state) { + case NGHTTP3_WT_STREAM_STATE_SESSION_ID: + assert(end - p > 0); + nread = nghttp3_read_varint(rvint, p, end, fin); + if (nread < 0) { + return NGHTTP3_ERR_H3_FRAME_ERROR; + } + + p += nread; + nconsumed += (size_t)nread; + if (rvint->left) { + /* TODO What should we do if unidirectional stream is closed + before reading Session ID? */ + break; + } + + rstate->left = rvint->acc; + nghttp3_varint_read_state_reset(rvint); + + /* rstate->left is Session ID */ + rv = nghttp3_conn_on_wt_stream(conn, stream, (int64_t)rstate->left); + if (rv != 0) { + if (rv != NGHTTP3_ERR_WT_SESSION_GONE) { + return rv; + } + + stream->rstate.state = NGHTTP3_WT_STREAM_STATE_IGN_REST; + + rv = nghttp3_conn_abort_stream(conn, stream, NGHTTP3_WT_SESSION_GONE); + if (rv != 0) { + return rv; + } + + nconsumed += (size_t)(end - p); + + return (nghttp3_ssize)nconsumed; + } + + rstate->state = NGHTTP3_WT_STREAM_STATE_DATA; + + wt_ctrl_stream = + nghttp3_conn_find_stream(conn, stream->wt.session->session_id); + + if (!(wt_ctrl_stream->wt.session->flags & + NGHTTP3_WT_SESSION_FLAG_CONFIRMED)) { + stream->flags |= NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED; + + if (p != end) { + rv = nghttp3_stream_buffer_data(stream, p, (size_t)(end - p)); + if (rv != 0) { + return rv; + } + } + + return (nghttp3_ssize)nconsumed; + } + + if (p == end) { + break; + } + + /* Fall through */ + case NGHTTP3_WT_STREAM_STATE_DATA: + rv = conn_call_recv_wt_data(conn, stream, p, (size_t)(end - p)); + if (rv != 0) { + return rv; + } + + break; + case NGHTTP3_WT_STREAM_STATE_IGN_REST: + nconsumed += (size_t)(end - p); + + return (nghttp3_ssize)nconsumed; + } + +almost_done: + if (fin) { + rv = conn_call_end_stream(conn, stream); + if (rv != 0) { + return rv; + } + } + + return (nghttp3_ssize)nconsumed; +} + +int nghttp3_conn_process_blocked_wt_stream_data(nghttp3_conn *conn, + nghttp3_stream *stream, + nghttp3_tstamp ts) { + nghttp3_buf *buf; + nghttp3_ssize nconsumed; + size_t nproc; + int rv; + size_t len; + + for (;;) { + len = nghttp3_ringbuf_len(&stream->inq); + if (len == 0) { + break; + } + + buf = nghttp3_ringbuf_get(&stream->inq, 0); + + if (nghttp3_stream_uni(stream->node.id)) { + nconsumed = nghttp3_conn_read_wt_stream_uni( + conn, stream, buf->pos, nghttp3_buf_len(buf), + len == 1 && (stream->flags & NGHTTP3_STREAM_FLAG_READ_EOF), ts); + } else { + nconsumed = nghttp3_conn_read_bidi( + conn, &nproc, stream, buf->pos, nghttp3_buf_len(buf), + len == 1 && (stream->flags & NGHTTP3_STREAM_FLAG_READ_EOF), ts); + } + + if (nconsumed < 0) { + return (int)nconsumed; + } + + rv = conn_call_deferred_consume(conn, stream, (size_t)nconsumed); + if (rv != 0) { + return rv; + } + + nghttp3_buf_free(buf, stream->mem); + nghttp3_ringbuf_pop_front(&stream->inq); + } + + return 0; +} + +int nghttp3_conn_shutdown_wt_session(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + uint64_t error_code) { + int rv; + + rv = + nghttp3_conn_shutdown_all_wt_data_streams(conn, wt_ctrl_stream, error_code); + if (rv != 0) { + return rv; + } + + wt_ctrl_stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + + return nghttp3_conn_abort_stream(conn, wt_ctrl_stream, error_code); +} + +int nghttp3_conn_shutdown_all_wt_data_streams(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + uint64_t error_code) { + nghttp3_wt_session *wt_session = wt_ctrl_stream->wt.session; + nghttp3_stream *stream; + int rv; + + for (stream = wt_session->head; stream; stream = stream->wt.next) { + rv = nghttp3_conn_shutdown_wt_data_stream(conn, stream, error_code); + if (rv != 0) { + return rv; + } + } + + return 0; +} + +int nghttp3_conn_shutdown_wt_data_stream(nghttp3_conn *conn, + nghttp3_stream *stream, + uint64_t error_code) { + if (stream->node.id & 0x2) { + stream->rstate.state = NGHTTP3_WT_STREAM_STATE_IGN_REST; + } else { + stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; + } + + return nghttp3_conn_abort_stream(conn, stream, error_code); +} + +int64_t nghttp3_conn_get_stream_wt_session_id(const nghttp3_conn *conn, + int64_t stream_id) { + const nghttp3_stream *stream = nghttp3_conn_find_stream(conn, stream_id); + + if (!stream || !nghttp3_stream_wt_data(stream)) { + return -1; + } + + return stream->wt.session->session_id; +} + +int nghttp3_conn_read_wt_ctrl_stream(nghttp3_conn *conn, + const nghttp3_stream *stream, + const uint8_t *src, size_t srclen) { + const uint8_t *p, *end; + nghttp3_wt_session *wts = stream->wt.session; + nghttp3_wt_ctrl_read_state *rstate = &wts->rstate; + nghttp3_varint_read_state *rvint = &rstate->rvint; + nghttp3_ssize nread; + nghttp3_exfr_cpsl *cpsl = &rstate->cpsl; + size_t len; + size_t i; + int rv; + + if (srclen == 0) { + return 0; + } + + p = src; + end = src + srclen; + + for (; p != end;) { + switch (rstate->state) { + case NGHTTP3_WT_CTRL_STREAM_STATE_TYPE: + assert(end - p > 0); + nread = nghttp3_read_varint(rvint, p, end, /* fin = */ 0); + + assert(nread > 0); + + p += nread; + if (rvint->left) { + return 0; + } + + rstate->cpsl.hd.type = rvint->acc; + + nghttp3_varint_read_state_reset(rvint); + rstate->state = NGHTTP3_WT_CTRL_STREAM_STATE_LENGTH; + if (p == end) { + return 0; + } + /* Fall through */ + case NGHTTP3_WT_CTRL_STREAM_STATE_LENGTH: + assert(end - p > 0); + nread = nghttp3_read_varint(rvint, p, end, /* fin = */ 0); + assert(nread > 0); + + p += nread; + if (rvint->left) { + return 0; + } + + rstate->left = rvint->acc; + nghttp3_varint_read_state_reset(rvint); + + switch (rstate->cpsl.hd.type) { + case NGHTTP3_EXFR_CPSL_WT_CLOSE_SESSION: + if (rstate->left < sizeof(uint32_t) || + rstate->left > sizeof(uint32_t) + /* largest message size */ 1024) { + /* TODO Find better error code */ + return NGHTTP3_ERR_H3_MESSAGE_ERROR; + } + + rstate->field_left = sizeof(uint32_t); + rstate->state = + NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_CODE; + + break; + default: + /* TODO Add rate limit after we implement all supported + capsules. */ + if (rstate->left == 0) { + nghttp3_wt_ctrl_read_state_reset(rstate); + break; + } + + rstate->state = NGHTTP3_WT_CTRL_STREAM_STATE_IGN; + } + + break; + case NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_CODE: + len = nghttp3_min(rstate->field_left, (size_t)(end - p)); + + for (i = 0; i < len; ++i) { + cpsl->wt_close_session.error_code <<= 8; + cpsl->wt_close_session.error_code += *p++; + } + + rstate->left -= len; + rstate->field_left -= len; + if (rstate->field_left) { + break; + } + + wts->rx.error_code = cpsl->wt_close_session.error_code; + + if (rstate->left == 0) { + if (conn->callbacks.recv_wt_close_session) { + rv = conn->callbacks.recv_wt_close_session( + conn, wts->session_id, wts->rx.error_code, NULL, 0, conn->user_data, + stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + + nghttp3_wt_ctrl_read_state_reset(rstate); + + return NGHTTP3_ERR_WT_SESSION_GONE; + } + + rstate->state = NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_MSG; + + wts->rx.error_msg.base = + nghttp3_mem_malloc(conn->mem, (size_t)rstate->left); + if (!wts->rx.error_msg.base) { + return NGHTTP3_ERR_NOMEM; + } + + if (p == end) { + return 0; + } + + /* Fall through */ + case NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_MSG: + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); + + memcpy(wts->rx.error_msg.base + wts->rx.error_msg.len, p, len); + wts->rx.error_msg.len += len; + + p += len; + rstate->left -= len; + + if (rstate->left) { + break; + } + + if (conn->callbacks.recv_wt_close_session) { + rv = conn->callbacks.recv_wt_close_session( + conn, wts->session_id, wts->rx.error_code, wts->rx.error_msg.base, + wts->rx.error_msg.len, conn->user_data, stream->user_data); + if (rv != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + } + + nghttp3_wt_ctrl_read_state_reset(rstate); + + return NGHTTP3_ERR_WT_SESSION_GONE; + case NGHTTP3_WT_CTRL_STREAM_STATE_IGN: + len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); + p += len; + rstate->left -= len; + + if (rstate->left) { + return 0; + } + + nghttp3_wt_ctrl_read_state_reset(rstate); + + break; + } + } + + return 0; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h index 6841b1c343a3..101def930b75 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.h @@ -202,8 +202,8 @@ nghttp3_ssize nghttp3_conn_read_qpack_decoder(nghttp3_conn *conn, const uint8_t *src, size_t srclen); -int nghttp3_conn_on_data(nghttp3_conn *conn, nghttp3_stream *stream, - const uint8_t *data, size_t datalen); +nghttp3_ssize nghttp3_conn_on_data(nghttp3_conn *conn, nghttp3_stream *stream, + const uint8_t *data, size_t datalen); int nghttp3_conn_on_priority_update(nghttp3_conn *conn, const nghttp3_frame_priority_update *fr); @@ -216,6 +216,8 @@ nghttp3_ssize nghttp3_conn_on_headers(nghttp3_conn *conn, int nghttp3_conn_on_settings_entry_received(nghttp3_conn *conn, const nghttp3_frame_settings *fr); +int nghttp3_conn_on_settings_received(nghttp3_conn *conn); + int nghttp3_conn_qpack_blocked_streams_push(nghttp3_conn *conn, nghttp3_stream *stream); @@ -233,10 +235,47 @@ void nghttp3_conn_unschedule_stream(nghttp3_conn *conn, nghttp3_stream *stream); int nghttp3_conn_reject_stream(nghttp3_conn *conn, nghttp3_stream *stream); +int nghttp3_conn_abort_stream(nghttp3_conn *conn, nghttp3_stream *stream, + uint64_t error_code); + /* * nghttp3_conn_get_next_tx_stream returns next stream to send. It * returns NULL if there is no such stream. */ nghttp3_stream *nghttp3_conn_get_next_tx_stream(nghttp3_conn *conn); +int nghttp3_conn_open_wt_session(nghttp3_conn *conn, nghttp3_stream *stream); + +int nghttp3_conn_on_wt_stream(nghttp3_conn *conn, nghttp3_stream *stream, + int64_t session_id); + +nghttp3_ssize nghttp3_conn_read_wt_stream_uni(nghttp3_conn *conn, + nghttp3_stream *stream, + const uint8_t *src, size_t srclen, + int fin, nghttp3_tstamp ts); + +int nghttp3_conn_on_wt_session_confirmed(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + nghttp3_tstamp ts); + +int nghttp3_conn_process_blocked_wt_stream_data(nghttp3_conn *conn, + nghttp3_stream *stream, + nghttp3_tstamp ts); + +int nghttp3_conn_shutdown_wt_session(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + uint64_t error_code); + +int nghttp3_conn_shutdown_all_wt_data_streams(nghttp3_conn *conn, + nghttp3_stream *wt_ctrl_stream, + uint64_t error_code); + +int nghttp3_conn_shutdown_wt_data_stream(nghttp3_conn *conn, + nghttp3_stream *stream, + uint64_t error_code); + +int nghttp3_conn_read_wt_ctrl_stream(nghttp3_conn *conn, + const nghttp3_stream *stream, + const uint8_t *src, size_t srclen); + #endif /* !defined(NGHTTP3_CONN_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c index 031ac78d815f..a90e1d25b70e 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.c @@ -79,6 +79,12 @@ const uint8_t *nghttp3_get_varint(int64_t *dest, const uint8_t *p) { return p; } +const uint8_t *nghttp3_get_uint32be(uint32_t *dest, const uint8_t *p) { + memcpy(dest, p, sizeof(*dest)); + *dest = ntohl(*dest); + return p + sizeof(*dest); +} + uint8_t *nghttp3_put_uint64be(uint8_t *p, uint64_t n) { n = nghttp3_htonl64(n); return nghttp3_cpymem(p, (const uint8_t *)&n, sizeof(n)); diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h index bd1c518fa663..bd6f29a55db5 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conv.h @@ -92,6 +92,13 @@ # define ntohs(N) _byteswap_ushort(N) #endif /* defined(WIN32) */ +/* + * nghttp3_get_uint32be reads 4 bytes from |p| as 32 bits unsigned + * integer encoded as network byte order, and stores it in the buffer + * pointed by |dest| in host byte order. It returns |p| + 4. + */ +const uint8_t *nghttp3_get_uint32be(uint32_t *dest, const uint8_t *p); + /* * nghttp3_put_uint64be writes |n| in host byte order in |p| in * network byte order. It returns the one beyond of the last written diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_err.c b/deps/ngtcp2/nghttp3/lib/nghttp3_err.c index eff6ea6a63a2..e6603c00f041 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_err.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_err.c @@ -76,6 +76,10 @@ const char *nghttp3_strerror(int liberr) { return "ERR_H3_STREAM_CREATION_ERROR"; case NGHTTP3_ERR_H3_EXCESSIVE_LOAD: return "ERR_H3_EXCESSIVE_LOAD"; + case NGHTTP3_ERR_WT_SESSION_GONE: + return "ERR_WT_SESSION_GONE"; + case NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED: + return "ERR_WT_BUFFERED_STREAM_REJECTED"; case NGHTTP3_ERR_NOMEM: return "ERR_NOMEM"; case NGHTTP3_ERR_CALLBACK_FAILURE: @@ -122,7 +126,12 @@ uint64_t nghttp3_err_infer_quic_app_error_code(int liberr) { return NGHTTP3_H3_EXCESSIVE_LOAD; case NGHTTP3_ERR_MALFORMED_HTTP_HEADER: case NGHTTP3_ERR_MALFORMED_HTTP_MESSAGING: + case NGHTTP3_ERR_H3_MESSAGE_ERROR: return NGHTTP3_H3_MESSAGE_ERROR; + case NGHTTP3_ERR_WT_SESSION_GONE: + return NGHTTP3_WT_SESSION_GONE; + case NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED: + return NGHTTP3_WT_BUFFERED_STREAM_REJECTED; default: return NGHTTP3_H3_GENERAL_PROTOCOL_ERROR; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c index 2efba7472c25..1742e0756fc6 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.c @@ -133,6 +133,44 @@ size_t nghttp3_frame_write_origin_len(uint64_t *ppayloadlen, payloadlen; } +uint8_t *nghttp3_frame_write_wt_stream(uint8_t *p, + const nghttp3_exfr_wt_stream *fr) { + p = nghttp3_put_uvarint(p, fr->type); + return nghttp3_put_uvarint(p, (uint64_t)fr->session_id); +} + +size_t nghttp3_frame_write_wt_stream_len(const nghttp3_exfr_wt_stream *fr) { + return nghttp3_put_uvarintlen(fr->type) + + nghttp3_put_uvarintlen((uint64_t)fr->session_id); +} + +uint8_t *nghttp3_frame_write_cpsl_wt_close_session( + uint8_t *p, const nghttp3_exfr_cpsl_wt_close_session *fr, + uint64_t payloadlen) { + p = nghttp3_frame_write_hd(p, NGHTTP3_FRAME_DATA, payloadlen); + p = nghttp3_frame_write_hd(p, fr->type, + sizeof(fr->error_code) + fr->error_msg.len); + p = nghttp3_put_uint32be(p, fr->error_code); + + if (fr->error_msg.len) { + p = nghttp3_cpymem(p, fr->error_msg.base, fr->error_msg.len); + } + + return p; +} + +size_t nghttp3_frame_write_cpsl_wt_close_session_len( + uint64_t *ppayloadlen, const nghttp3_exfr_cpsl_wt_close_session *fr) { + size_t cpsl_payloadlen = sizeof(fr->error_code) + fr->error_msg.len; + size_t payloadlen = nghttp3_put_uvarintlen(fr->type) + + nghttp3_put_uvarintlen(cpsl_payloadlen) + cpsl_payloadlen; + + *ppayloadlen = payloadlen; + + return nghttp3_put_uvarintlen(NGHTTP3_FRAME_DATA) + + nghttp3_put_uvarintlen(payloadlen) + payloadlen; +} + int nghttp3_nva_copy(nghttp3_nv **pnva, const nghttp3_nv *nva, size_t nvlen, const nghttp3_mem *mem) { size_t i; diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h index 7806cadbcf5f..77b4d37b8fb2 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_frame.h @@ -46,6 +46,24 @@ #define NGHTTP3_FRAME_PRIORITY_UPDATE_PUSH_ID 0x0F0701U /* ORIGIN: https://datatracker.ietf.org/doc/html/rfc9412 */ #define NGHTTP3_FRAME_ORIGIN 0x0CU +/* WebTransport extended frame type */ +#define NGHTTP3_FRAME_EX_WT 0x4000000000000001ULL +/* WT_STREAM: + https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-14 */ +#define NGHTTP3_EXFR_WT_STREAM_BIDI 0x41U +#define NGHTTP3_EXFR_WT_STREAM_UNI 0x54U +#define NGHTTP3_EXFR_WT_STREAM_DATA 0x00U + +/* HTTP Capsule extended frame type */ +#define NGHTTP3_FRAME_EX_CPSL 0x4000000000000002ULL +#define NGHTTP3_EXFR_CPSL_WT_CLOSE_SESSION 0x2843U +#define NGHTTP3_EXFR_CPSL_WT_DRAIN_SESSION 0x78AEU +#define NGHTTP3_EXFR_CPSL_WT_MAX_STREAMS_BIDI 0x190B4D3FU +#define NGHTTP3_EXFR_CPSL_WT_MAX_STREAMS_UNI 0x190B4D40U +#define NGHTTP3_EXFR_CPSL_WT_STREAMS_BLOCKED_BIDI 0x190B4D43U +#define NGHTTP3_EXFR_CPSL_WT_STREAMS_BLOCKED_UNI 0x190B4D44U +#define NGHTTP3_EXFR_CPSL_WT_MAX_DATA 0x190B4D3DU +#define NGHTTP3_EXFR_CPSL_WT_DATA_BLOCKED 0x190B4D41U /* Frame types that are reserved for HTTP/2, and must not be used in HTTP/3. */ @@ -76,6 +94,14 @@ typedef struct nghttp3_frame_headers { #define NGHTTP3_SETTINGS_ID_QPACK_BLOCKED_STREAMS 0x07U #define NGHTTP3_SETTINGS_ID_ENABLE_CONNECT_PROTOCOL 0x08U #define NGHTTP3_SETTINGS_ID_H3_DATAGRAM 0x33U +/* https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-15 */ +#define NGHTTP3_SETTINGS_ID_WT_ENABLED 0x2C7CF000U +/* https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-14 */ +#define NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS 0x14E9CD29U +/* https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-07 */ +#define NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS_DRAFT7 0xC671706AU +/* https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3-02 */ +#define NGHTTP3_SETTINGS_ID_ENABLE_WEBTRANSPORT_DRAFT2 0x2B603742U #define NGHTTP3_H2_SETTINGS_ID_ENABLE_PUSH 0x2U #define NGHTTP3_H2_SETTINGS_ID_MAX_CONCURRENT_STREAMS 0x3U @@ -132,6 +158,42 @@ typedef struct nghttp3_frame_origin { nghttp3_vec origin_list; } nghttp3_frame_origin; +typedef struct nghttp3_exfr_hd { + uint64_t type; +} nghttp3_exfr_hd; + +typedef struct nghttp3_exfr_wt_stream { + uint64_t type; + int64_t session_id; + nghttp3_data_reader dr; +} nghttp3_exfr_wt_stream; + +typedef union nghttp3_exfr_wt { + nghttp3_exfr_hd hd; + nghttp3_exfr_wt_stream wt_stream; +} nghttp3_exfr_wt; + +typedef struct nghttp3_frame_ex_wt { + uint64_t type; + nghttp3_exfr_wt fr; +} nghttp3_frame_ex_wt; + +typedef struct nghttp3_exfr_cpsl_wt_close_session { + uint64_t type; + nghttp3_vec error_msg; + uint32_t error_code; +} nghttp3_exfr_cpsl_wt_close_session; + +typedef union nghttp3_exfr_cpsl { + nghttp3_exfr_hd hd; + nghttp3_exfr_cpsl_wt_close_session wt_close_session; +} nghttp3_exfr_cpsl; + +typedef struct nghttp3_frame_ex_cpsl { + uint64_t type; + nghttp3_exfr_cpsl fr; +} nghttp3_frame_ex_cpsl; + typedef union nghttp3_frame { nghttp3_frame_hd hd; nghttp3_frame_data data; @@ -140,6 +202,8 @@ typedef union nghttp3_frame { nghttp3_frame_goaway goaway; nghttp3_frame_priority_update priority_update; nghttp3_frame_origin origin; + nghttp3_frame_ex_wt wt; + nghttp3_frame_ex_cpsl cpsl; } nghttp3_frame; /* @@ -233,6 +297,18 @@ uint8_t *nghttp3_frame_write_origin(uint8_t *dest, size_t nghttp3_frame_write_origin_len(uint64_t *ppayloadlen, const nghttp3_frame_origin *fr); +uint8_t *nghttp3_frame_write_wt_stream(uint8_t *dest, + const nghttp3_exfr_wt_stream *fr); + +size_t nghttp3_frame_write_wt_stream_len(const nghttp3_exfr_wt_stream *fr); + +uint8_t *nghttp3_frame_write_cpsl_wt_close_session( + uint8_t *dest, const nghttp3_exfr_cpsl_wt_close_session *fr, + uint64_t payloadlen); + +size_t nghttp3_frame_write_cpsl_wt_close_session_len( + uint64_t *ppayloadlen, const nghttp3_exfr_cpsl_wt_close_session *fr); + /* * nghttp3_nva_copy copies name/value pairs from |nva|, which contains * |nvlen| pairs, to |*nva_ptr|, which is dynamically allocated so diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_http.c b/deps/ngtcp2/nghttp3/lib/nghttp3_http.c index 4194a404b33f..56dc0426aa73 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_http.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_http.c @@ -384,6 +384,12 @@ static int http_request_on_header(nghttp3_http_state *http, !check_pseudo_header(http, nv, NGHTTP3_HTTP_FLAG__PROTOCOL)) { return NGHTTP3_ERR_MALFORMED_HTTP_HEADER; } + + if (lstrieq("webtransport-h3", nv->value->base, nv->value->len) || + lstrieq("webtransport", nv->value->base, nv->value->len)) { + http->flags |= NGHTTP3_HTTP_FLAG_WEBTRANSPORT; + } + break; case NGHTTP3_QPACK_TOKEN_HOST: if (!check_authority(nv->value->base, nv->value->len)) { diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_http.h b/deps/ngtcp2/nghttp3/lib/nghttp3_http.h index 2bdf3110027c..ec32414c1d1d 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_http.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_http.h @@ -82,6 +82,8 @@ typedef struct nghttp3_http_state nghttp3_http_state; while parsing priority header field. */ #define NGHTTP3_HTTP_FLAG_BAD_PRIORITY 0x010000U +#define NGHTTP3_HTTP_FLAG_WEBTRANSPORT 0x020000U + /* * This function is called when HTTP header field |nv| received for * |http|. This function will validate |nv| against the current state diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c b/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c index 4ab9b81fde0c..c1f3cdf9b937 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c @@ -226,21 +226,22 @@ static int ksl_split_node(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, size_t i) { * Out of memory. */ static int ksl_split_root(nghttp3_ksl *ksl) { - nghttp3_ksl_blk *rblk, *lblk, *nroot; - - nroot = ksl_blk_objalloc_new(ksl); - if (nroot == NULL) { - return NGHTTP3_ERR_NOMEM; - } + nghttp3_ksl_blk *rblk = NULL, *lblk, *nroot = NULL; rblk = ksl_split_blk(ksl, ksl->root); if (rblk == NULL) { - ksl_blk_objalloc_del(ksl, nroot); return NGHTTP3_ERR_NOMEM; } lblk = ksl->root; + nroot = ksl_blk_objalloc_new(ksl); + + if (nroot == NULL) { + ksl_blk_objalloc_del(ksl, rblk); + return NGHTTP3_ERR_NOMEM; + } + nroot->next = nroot->prev = NULL; nroot->n = 2; nroot->leaf = 0; diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c index 8e950a201d89..59b633ea0572 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c @@ -2820,7 +2820,6 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, int busy = 0; const nghttp3_mem *mem = decoder->ctx.mem; nghttp3_ssize nread; - size_t huff_declen; int rfin; if (decoder->ctx.bad) { @@ -2959,16 +2958,13 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, } if (decoder->rstate.huffman_encoded) { - huff_declen = nghttp3_qpack_huffman_estimate_decode_length( - (size_t)decoder->rstate.left); - if (huff_declen > NGHTTP3_QPACK_MAX_NAMELEN) { - rv = NGHTTP3_ERR_QPACK_HEADER_TOO_LARGE; - goto fail; - } - decoder->state = NGHTTP3_QPACK_ES_STATE_READ_NAME_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&decoder->rstate.huffman_ctx); - rv = nghttp3_rcbuf_new(&decoder->rstate.name, huff_declen + 1, mem); + rv = nghttp3_rcbuf_new(&decoder->rstate.name, + nghttp3_qpack_huffman_estimate_decode_length( + (size_t)decoder->rstate.left) + + 1, + mem); } else { decoder->state = NGHTTP3_QPACK_ES_STATE_READ_NAME; rv = nghttp3_rcbuf_new(&decoder->rstate.name, @@ -3047,16 +3043,13 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, } if (decoder->rstate.huffman_encoded) { - huff_declen = nghttp3_qpack_huffman_estimate_decode_length( - (size_t)decoder->rstate.left); - if (huff_declen > NGHTTP3_QPACK_MAX_VALUELEN) { - rv = NGHTTP3_ERR_QPACK_HEADER_TOO_LARGE; - goto fail; - } - decoder->state = NGHTTP3_QPACK_ES_STATE_READ_VALUE_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&decoder->rstate.huffman_ctx); - rv = nghttp3_rcbuf_new(&decoder->rstate.value, huff_declen + 1, mem); + rv = nghttp3_rcbuf_new(&decoder->rstate.value, + nghttp3_qpack_huffman_estimate_decode_length( + (size_t)decoder->rstate.left) + + 1, + mem); } else { decoder->state = NGHTTP3_QPACK_ES_STATE_READ_VALUE; rv = nghttp3_rcbuf_new(&decoder->rstate.value, @@ -3349,7 +3342,6 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, nghttp3_ssize nread; int rfin; const nghttp3_mem *mem = decoder->ctx.mem; - size_t huff_declen; if (decoder->ctx.bad) { return NGHTTP3_ERR_QPACK_FATAL; @@ -3573,16 +3565,13 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, } if (sctx->rstate.huffman_encoded) { - huff_declen = nghttp3_qpack_huffman_estimate_decode_length( - (size_t)sctx->rstate.left); - if (huff_declen > NGHTTP3_QPACK_MAX_NAMELEN) { - rv = NGHTTP3_ERR_QPACK_HEADER_TOO_LARGE; - goto fail; - } - sctx->state = NGHTTP3_QPACK_RS_STATE_READ_NAME_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&sctx->rstate.huffman_ctx); - rv = nghttp3_rcbuf_new(&sctx->rstate.name, huff_declen + 1, mem); + rv = nghttp3_rcbuf_new(&sctx->rstate.name, + nghttp3_qpack_huffman_estimate_decode_length( + (size_t)sctx->rstate.left) + + 1, + mem); } else { sctx->state = NGHTTP3_QPACK_RS_STATE_READ_NAME; rv = nghttp3_rcbuf_new(&sctx->rstate.name, @@ -3659,16 +3648,13 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, } if (sctx->rstate.huffman_encoded) { - huff_declen = nghttp3_qpack_huffman_estimate_decode_length( - (size_t)sctx->rstate.left); - if (huff_declen > NGHTTP3_QPACK_MAX_VALUELEN) { - rv = NGHTTP3_ERR_QPACK_HEADER_TOO_LARGE; - goto fail; - } - sctx->state = NGHTTP3_QPACK_RS_STATE_READ_VALUE_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&sctx->rstate.huffman_ctx); - rv = nghttp3_rcbuf_new(&sctx->rstate.value, huff_declen + 1, mem); + rv = nghttp3_rcbuf_new(&sctx->rstate.value, + nghttp3_qpack_huffman_estimate_decode_length( + (size_t)sctx->rstate.left) + + 1, + mem); } else { sctx->state = NGHTTP3_QPACK_RS_STATE_READ_VALUE; rv = nghttp3_rcbuf_new(&sctx->rstate.value, diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h index 934cae77c1bf..2b8dca9d5cf7 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h @@ -42,11 +42,11 @@ #define NGHTTP3_QPACK_INT_MAX ((1ULL << 62) - 1) -/* NGHTTP3_QPACK_MAX_NAMELEN is the maximum (estimated uncompressed) - length of header name this library can decode. */ +/* NGHTTP3_QPACK_MAX_NAMELEN is the maximum (compressed) length of + header name this library can decode. */ #define NGHTTP3_QPACK_MAX_NAMELEN 256 -/* NGHTTP3_QPACK_MAX_VALUELEN is the maximum (estimated uncompressed) - length of header value this library can decode. */ +/* NGHTTP3_QPACK_MAX_VALUELEN is the maximum (compressed) length of + header value this library can decode. */ #define NGHTTP3_QPACK_MAX_VALUELEN 65536 /* NGHTTP3_QPACK_MAX_ENCODERLEN is the maximum encoder stream length that a decoder accepts without completely processing a single field diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c index 09298006f4f6..6fc40fe9d3b1 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.c @@ -36,6 +36,7 @@ #include "nghttp3_http.h" #include "nghttp3_vec.h" #include "nghttp3_unreachable.h" +#include "nghttp3_wt.h" /* NGHTTP3_STREAM_MAX_COPY_THRES is the maximum size of buffer which makes a copy to outq. */ @@ -167,6 +168,10 @@ void nghttp3_stream_del(nghttp3_stream *stream) { delete_frq(&stream->frq, stream->mem); nghttp3_tnode_free(&stream->node); + if (nghttp3_stream_wt_ctrl(stream)) { + nghttp3_wt_session_del(stream->wt.session, stream->mem); + } + nghttp3_objalloc_stream_release(stream->stream_objalloc, stream); } @@ -294,6 +299,47 @@ int nghttp3_stream_fill_outq(nghttp3_stream *stream) { return rv; } + break; + case NGHTTP3_FRAME_EX_WT: + switch (fr->wt.fr.hd.type) { + case NGHTTP3_EXFR_WT_STREAM_BIDI: + case NGHTTP3_EXFR_WT_STREAM_UNI: + rv = nghttp3_stream_write_wt_stream(stream, &fr->wt.fr.wt_stream); + if (rv != 0) { + return rv; + } + + fr->wt.fr.wt_stream.type = NGHTTP3_EXFR_WT_STREAM_DATA; + + /* fall through */ + case NGHTTP3_EXFR_WT_STREAM_DATA: + rv = nghttp3_stream_write_wt_stream_data(stream, &data_eof, + &fr->wt.fr.wt_stream); + if (rv != 0) { + return rv; + } + + if ((stream->flags & NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED) || + !data_eof) { + return 0; + } + + break; + } + + break; + case NGHTTP3_FRAME_EX_CPSL: + switch (fr->cpsl.fr.hd.type) { + case NGHTTP3_EXFR_CPSL_WT_CLOSE_SESSION: + rv = nghttp3_stream_write_cpsl_wt_close_session( + stream, &fr->cpsl.fr.wt_close_session); + if (rv != 0) { + return rv; + } + + break; + } + break; default: /* TODO Not implemented */ @@ -372,6 +418,31 @@ int nghttp3_stream_write_settings(nghttp3_stream *stream, ++fr.niv; } + if (local_settings->wt_enabled) { + /* For client, only draft version sends SETTINGS_WT_ENABLED. */ + ents[fr.niv++] = (nghttp3_settings_entry){ + .id = NGHTTP3_SETTINGS_ID_WT_ENABLED, + .value = 1, + }; + + /* compat for pre draft-15 */ + ents[fr.niv++] = (nghttp3_settings_entry){ + .id = NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS, + .value = 1, + }; + + ents[fr.niv++] = (nghttp3_settings_entry){ + .id = NGHTTP3_SETTINGS_ID_WT_MAX_SESSIONS_DRAFT7, + .value = 1, + }; + + /* compat for ancient draft */ + ents[fr.niv++] = (nghttp3_settings_entry){ + .id = NGHTTP3_SETTINGS_ID_ENABLE_WEBTRANSPORT_DRAFT2, + .value = 1, + }; + } + len = nghttp3_frame_write_settings_len(&payloadlen, &fr); rv = nghttp3_stream_ensure_chunk(stream, len); @@ -478,6 +549,150 @@ int nghttp3_stream_write_origin(nghttp3_stream *stream, return nghttp3_stream_outq_add(stream, &tbuf); } +int nghttp3_stream_write_wt_stream(nghttp3_stream *stream, + const nghttp3_exfr_wt_stream *fr) { + size_t len; + int rv; + nghttp3_buf *chunk; + nghttp3_typed_buf tbuf; + + len = nghttp3_frame_write_wt_stream_len(fr); + + rv = nghttp3_stream_ensure_chunk(stream, len); + if (rv != 0) { + return rv; + } + + chunk = nghttp3_stream_get_chunk(stream); + nghttp3_typed_buf_shared_init(&tbuf, chunk); + + chunk->last = nghttp3_frame_write_wt_stream(chunk->last, fr); + + tbuf.buf.last = chunk->last; + + return nghttp3_stream_outq_add(stream, &tbuf); +} + +int nghttp3_stream_write_wt_stream_data(nghttp3_stream *stream, int *peof, + const nghttp3_exfr_wt_stream *fr) { + int rv; + nghttp3_typed_buf tbuf; + nghttp3_buf buf; + nghttp3_read_data_callback read_data = fr->dr.read_data; + nghttp3_conn *conn = stream->conn; + uint64_t datalen; + uint32_t flags = 0; + nghttp3_vec vec[8]; + nghttp3_vec *v; + nghttp3_ssize sveccnt; + size_t i; + + assert(!(stream->flags & NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED)); + assert(read_data); + assert(conn); + + *peof = 0; + + sveccnt = read_data(conn, stream->node.id, vec, nghttp3_arraylen(vec), &flags, + conn->user_data, stream->user_data); + if (sveccnt < 0) { + if (sveccnt == NGHTTP3_ERR_WOULDBLOCK) { + stream->flags |= NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED; + return 0; + } + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + rv = nghttp3_vec_len_uvarint(&datalen, vec, (size_t)sveccnt); + if (rv != 0) { + return NGHTTP3_ERR_STREAM_DATA_OVERFLOW; + } + + assert(datalen || flags & NGHTTP3_DATA_FLAG_EOF); + + if (flags & NGHTTP3_DATA_FLAG_EOF) { + *peof = 1; + + stream->flags |= NGHTTP3_STREAM_FLAG_WRITE_END_STREAM; + if (datalen == 0) { + if (nghttp3_stream_outq_write_done(stream)) { + /* If this is the last data and its is 0 length, we don't need + send data. We rely on the non-emptiness of outq to + schedule stream, so add empty tbuf to outq to just send + fin. */ + nghttp3_buf_init(&buf); + nghttp3_typed_buf_init(&tbuf, &buf, NGHTTP3_BUF_TYPE_PRIVATE); + return nghttp3_stream_outq_add(stream, &tbuf); + } + + /* We are going to send data, but nothing to send this time. */ + + return 0; + } + } + + assert(datalen); + + for (i = 0; i < (size_t)sveccnt; ++i) { + v = &vec[i]; + if (v->len == 0) { + continue; + } + nghttp3_buf_wrap_init(&buf, v->base, v->len); + buf.last = buf.end; + nghttp3_typed_buf_init(&tbuf, &buf, NGHTTP3_BUF_TYPE_ALIEN); + rv = nghttp3_stream_outq_add(stream, &tbuf); + if (rv != 0) { + return rv; + } + } + + return 0; +} + +int nghttp3_stream_write_cpsl_wt_close_session( + nghttp3_stream *stream, const nghttp3_exfr_cpsl_wt_close_session *fr) { + int rv; + nghttp3_buf *chunk; + nghttp3_buf buf; + nghttp3_typed_buf tbuf; + size_t cpsl_payloadlen = sizeof(fr->error_code) + fr->error_msg.len; + size_t fr_hdlen = nghttp3_frame_write_hd_len(fr->type, cpsl_payloadlen); + uint64_t payloadlen = fr_hdlen + cpsl_payloadlen; + + rv = nghttp3_stream_ensure_chunk( + stream, nghttp3_frame_write_hd_len(NGHTTP3_FRAME_DATA, payloadlen) + + fr_hdlen + sizeof(fr->error_code)); + if (rv != 0) { + return rv; + } + + chunk = nghttp3_stream_get_chunk(stream); + nghttp3_typed_buf_shared_init(&tbuf, chunk); + + chunk->last = + nghttp3_frame_write_hd(chunk->last, NGHTTP3_FRAME_DATA, payloadlen); + chunk->last = nghttp3_frame_write_hd(chunk->last, fr->type, cpsl_payloadlen); + chunk->last = nghttp3_put_uint32be(chunk->last, fr->error_code); + + tbuf.buf.last = chunk->last; + + rv = nghttp3_stream_outq_add(stream, &tbuf); + if (rv != 0) { + return rv; + } + + if (fr->error_msg.len == 0) { + return 0; + } + + nghttp3_buf_wrap_init(&buf, fr->error_msg.base, fr->error_msg.len); + buf.last = buf.end; + nghttp3_typed_buf_init(&tbuf, &buf, NGHTTP3_BUF_TYPE_ALIEN_NO_ACK); + + return nghttp3_stream_outq_add(stream, &tbuf); +} + int nghttp3_stream_write_headers(nghttp3_stream *stream, const nghttp3_frame_headers *fr) { nghttp3_conn *conn = stream->conn; @@ -848,6 +1063,11 @@ int nghttp3_stream_require_schedule(const nghttp3_stream *stream) { !(stream->flags & NGHTTP3_STREAM_FLAG_READ_DATA_BLOCKED)); } +int nghttp3_stream_schedulable(const nghttp3_stream *stream) { + return !nghttp3_stream_uni(stream->node.id) || + stream->type == NGHTTP3_STREAM_TYPE_WT_STREAM; +} + size_t nghttp3_stream_writev(nghttp3_stream *stream, int *pfin, nghttp3_vec *vec, size_t veccnt) { nghttp3_ringbuf *outq = &stream->outq; @@ -1235,8 +1455,25 @@ int nghttp3_stream_empty_headers_allowed(const nghttp3_stream *stream) { } } +int nghttp3_stream_critical(const nghttp3_stream *stream) { + return nghttp3_stream_uni(stream->node.id) && + (stream->type == NGHTTP3_STREAM_TYPE_CONTROL || + stream->type == NGHTTP3_STREAM_TYPE_QPACK_ENCODER || + stream->type == NGHTTP3_STREAM_TYPE_QPACK_DECODER); +} + int nghttp3_stream_uni(int64_t stream_id) { return (stream_id & 0x2) != 0; } +int nghttp3_stream_wt_ctrl(const nghttp3_stream *stream) { + return stream->wt.session && + stream->wt.session->session_id == stream->node.id; +} + +int nghttp3_stream_wt_data(const nghttp3_stream *stream) { + return stream->wt.session && + stream->wt.session->session_id != stream->node.id; +} + int nghttp3_client_stream_bidi(int64_t stream_id) { return (stream_id & 0x3) == 0; } @@ -1248,3 +1485,7 @@ int nghttp3_client_stream_uni(int64_t stream_id) { int nghttp3_server_stream_uni(int64_t stream_id) { return (stream_id & 0x3) == 0x3; } + +int nghttp3_server_stream_bidi(int64_t stream_id) { + return (stream_id & 0x3) == 0x1; +} diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h index d313b1c192b9..0ea948e81159 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h @@ -56,6 +56,7 @@ typedef uint64_t nghttp3_stream_type; #define NGHTTP3_STREAM_TYPE_PUSH 0x01U #define NGHTTP3_STREAM_TYPE_QPACK_ENCODER 0x02U #define NGHTTP3_STREAM_TYPE_QPACK_DECODER 0x03U +#define NGHTTP3_STREAM_TYPE_WT_STREAM 0x54U #define NGHTTP3_STREAM_TYPE_UNKNOWN UINT64_MAX typedef enum nghttp3_ctrl_stream_state { @@ -78,10 +79,19 @@ typedef enum nghttp3_req_stream_state { NGHTTP3_REQ_STREAM_STATE_FRAME_LENGTH, NGHTTP3_REQ_STREAM_STATE_DATA, NGHTTP3_REQ_STREAM_STATE_HEADERS, + NGHTTP3_REQ_STREAM_STATE_BEFORE_WT_DATA, + NGHTTP3_REQ_STREAM_STATE_WT_DATA, NGHTTP3_REQ_STREAM_STATE_IGN_FRAME, NGHTTP3_REQ_STREAM_STATE_IGN_REST, } nghttp3_req_stream_state; +/* stream state for WebTransport unidirectional data stream */ +typedef enum nghttp3_wt_stream_state { + NGHTTP3_WT_STREAM_STATE_SESSION_ID, + NGHTTP3_WT_STREAM_STATE_DATA, + NGHTTP3_WT_STREAM_STATE_IGN_REST, +} nghttp3_wt_stream_state; + typedef struct nghttp3_varint_read_state { uint64_t acc; size_t left; @@ -95,6 +105,8 @@ typedef struct nghttp3_stream_read_state { int state; } nghttp3_stream_read_state; +typedef struct nghttp3_wt_session nghttp3_wt_session; + /* NGHTTP3_STREAM_FLAG_NONE indicates that no flag is set. */ #define NGHTTP3_STREAM_FLAG_NONE 0x0000U /* NGHTTP3_STREAM_FLAG_TYPE_IDENTIFIED is set when a unidirectional @@ -128,6 +140,18 @@ typedef struct nghttp3_stream_read_state { /* NGHTTP3_STREAM_FLAG_PRIORITY_UPDATE_RECVED indicates that server received PRIORITY_UPDATE frame for this stream. */ #define NGHTTP3_STREAM_FLAG_PRIORITY_UPDATE_RECVED 0x0800U +/* NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA indicates that the stream may be + WebTransport data stream. */ +#define NGHTTP3_STREAM_FLAG_MAYBE_WT_DATA 0x1000U +/* NGHTTP3_STREAM_FLAG_WT_DATA indicates that the stream is + WebTransport data stream. */ +#define NGHTTP3_STREAM_FLAG_WT_DATA 0x2000U +/* NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED indicates that the stream is + blocked because WebTransport session has not been established. */ +#define NGHTTP3_STREAM_FLAG_WT_SESSION_BLOCKED 0x4000U +/* NGHTTP3_STREAM_FLAG_RESP_SUBMITTED indicates that HTTP/3 response + has been submitted via nghttp3_conn_submit_response. */ +#define NGHTTP3_STREAM_FLAG_RESP_SUBMITTED 0x8000U typedef enum nghttp3_stream_http_state { NGHTTP3_HTTP_STATE_NONE, @@ -235,6 +259,12 @@ struct nghttp3_stream { nghttp3_http_state http; } rx; + struct { + nghttp3_wt_session *session; + nghttp3_stream *prev; + nghttp3_stream *next; + } wt; + uint16_t flags; }; @@ -310,6 +340,15 @@ int nghttp3_stream_write_priority_update( int nghttp3_stream_write_origin(nghttp3_stream *stream, const nghttp3_frame_origin *fr); +int nghttp3_stream_write_wt_stream(nghttp3_stream *stream, + const nghttp3_exfr_wt_stream *fr); + +int nghttp3_stream_write_wt_stream_data(nghttp3_stream *stream, int *peof, + const nghttp3_exfr_wt_stream *fr); + +int nghttp3_stream_write_cpsl_wt_close_session( + nghttp3_stream *stream, const nghttp3_exfr_cpsl_wt_close_session *frent); + int nghttp3_stream_ensure_chunk(nghttp3_stream *stream, size_t need); nghttp3_buf *nghttp3_stream_get_chunk(nghttp3_stream *stream); @@ -344,6 +383,8 @@ int nghttp3_stream_is_active(nghttp3_stream *stream); */ int nghttp3_stream_require_schedule(const nghttp3_stream *stream); +int nghttp3_stream_schedulable(const nghttp3_stream *stream); + int nghttp3_stream_buffer_data(nghttp3_stream *stream, const uint8_t *src, size_t srclen); @@ -358,6 +399,12 @@ int nghttp3_stream_transit_rx_http_state(nghttp3_stream *stream, int nghttp3_stream_empty_headers_allowed(const nghttp3_stream *stream); +int nghttp3_stream_wt_ctrl(const nghttp3_stream *stream); + +int nghttp3_stream_wt_data(const nghttp3_stream *stream); + +int nghttp3_stream_critical(const nghttp3_stream *stream); + /* * nghttp3_stream_uni returns nonzero if stream identified by * |stream_id| is unidirectional. @@ -382,4 +429,6 @@ int nghttp3_client_stream_uni(int64_t stream_id); */ int nghttp3_server_stream_uni(int64_t stream_id); +int nghttp3_server_stream_bidi(int64_t stream_id); + #endif /* !defined(NGHTTP3_STREAM_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_wt.c b/deps/ngtcp2/nghttp3/lib/nghttp3_wt.c new file mode 100644 index 000000000000..91331206c5c6 --- /dev/null +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_wt.c @@ -0,0 +1,95 @@ +/* + * nghttp3 + * + * Copyright (c) 2025 nghttp3 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "nghttp3_wt.h" + +#include + +#include "nghttp3_mem.h" + +int nghttp3_wt_session_new(nghttp3_wt_session **pwts, int64_t session_id, + const nghttp3_mem *mem) { + *pwts = nghttp3_mem_malloc(mem, sizeof(**pwts)); + if (*pwts == NULL) { + return NGHTTP3_ERR_NOMEM; + } + + **pwts = (nghttp3_wt_session){ + .session_id = session_id, + }; + + return 0; +} + +void nghttp3_wt_session_del(nghttp3_wt_session *wts, const nghttp3_mem *mem) { + if (!wts) { + return; + } + + nghttp3_mem_free(mem, wts->rx.error_msg.base); + nghttp3_mem_free(mem, wts->tx.error_msg.base); + + nghttp3_mem_free(mem, wts); +} + +void nghttp3_wt_session_add_stream(nghttp3_wt_session *wts, + nghttp3_stream *stream) { + assert(!stream->wt.session); + assert(!stream->wt.prev); + assert(!stream->wt.next); + + stream->wt.session = wts; + stream->flags |= NGHTTP3_STREAM_FLAG_WT_DATA; + + if (wts->head) { + stream->wt.next = wts->head; + wts->head->wt.prev = stream; + } + + wts->head = stream; +} + +void nghttp3_wt_session_remove_stream(nghttp3_wt_session *wts, + nghttp3_stream *stream) { + assert(stream->wt.session); + + if (stream->wt.prev) { + stream->wt.prev->wt.next = stream->wt.next; + } + + if (stream->wt.next) { + stream->wt.next->wt.prev = stream->wt.prev; + } + + if (wts->head == stream) { + wts->head = stream->wt.next; + } + + stream->wt.session = NULL; + stream->wt.prev = stream->wt.next = NULL; +} + +void nghttp3_wt_ctrl_read_state_reset(nghttp3_wt_ctrl_read_state *rstate) { + *rstate = (nghttp3_wt_ctrl_read_state){0}; +} diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_wt.h b/deps/ngtcp2/nghttp3/lib/nghttp3_wt.h new file mode 100644 index 000000000000..eb4b2df13fc8 --- /dev/null +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_wt.h @@ -0,0 +1,86 @@ +/* + * nghttp3 + * + * Copyright (c) 2025 nghttp3 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGHTTP3_WT_H +#define NGHTTP3_WT_H + +#ifdef HAVE_CONFIG_H +# include +#endif /* defined(HAVE_CONFIG_H) */ + +#include + +#include "nghttp3_stream.h" + +/* NGHTTP3_WT_SESSION_FLAG_CONFIRMED indicates that WebTransport + session has been established. */ +#define NGHTTP3_WT_SESSION_FLAG_CONFIRMED 0x1 +/* NGHTTP3_WT_SESSION_FLAG_RESP_SUBMITTED indicates that HTTP/3 + response has been submitted via nghttp3_conn_submit_wt_response. */ +#define NGHTTP3_WT_SESSION_FLAG_RESP_SUBMITTED 0x2 + +typedef enum nghttp3_wt_ctrl_stream_state { + NGHTTP3_WT_CTRL_STREAM_STATE_TYPE, + NGHTTP3_WT_CTRL_STREAM_STATE_LENGTH, + NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_CODE, + NGHTTP3_WT_CTRL_STREAM_STATE_WT_CLOSE_SESSION_ERROR_MSG, + NGHTTP3_WT_CTRL_STREAM_STATE_IGN, +} nghttp3_wt_ctrl_stream_state; + +typedef struct nghttp3_wt_ctrl_read_state { + nghttp3_varint_read_state rvint; + nghttp3_exfr_cpsl cpsl; + uint64_t left; + size_t field_left; + int state; +} nghttp3_wt_ctrl_read_state; + +typedef struct nghttp3_wt_session { + nghttp3_wt_ctrl_read_state rstate; + struct { + nghttp3_vec error_msg; + } tx; + struct { + nghttp3_vec error_msg; + uint32_t error_code; + } rx; + int64_t session_id; + nghttp3_stream *head; + uint32_t flags; +} nghttp3_wt_session; + +void nghttp3_wt_session_add_stream(nghttp3_wt_session *wts, + nghttp3_stream *stream); + +void nghttp3_wt_session_remove_stream(nghttp3_wt_session *wts, + nghttp3_stream *stream); + +int nghttp3_wt_session_new(nghttp3_wt_session **pwts, int64_t stream_id, + const nghttp3_mem *mem); + +void nghttp3_wt_session_del(nghttp3_wt_session *wts, const nghttp3_mem *mem); + +void nghttp3_wt_ctrl_read_state_reset(nghttp3_wt_ctrl_read_state *rstate); + +#endif /* !defined(NGHTTP3_WT_H) */ From 2f3570e9abff1c807d46ee6fca2250e1243f3faa Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 20 Jun 2026 09:52:12 +0200 Subject: [PATCH 02/37] nghttp3: add wt to gyp --- deps/ngtcp2/ngtcp2.gyp | 1 + 1 file changed, 1 insertion(+) diff --git a/deps/ngtcp2/ngtcp2.gyp b/deps/ngtcp2/ngtcp2.gyp index 7ae627924851..939b9b9eba44 100644 --- a/deps/ngtcp2/ngtcp2.gyp +++ b/deps/ngtcp2/ngtcp2.gyp @@ -90,6 +90,7 @@ 'nghttp3/lib/nghttp3_unreachable.c', 'nghttp3/lib/nghttp3_vec.c', 'nghttp3/lib/nghttp3_version.c', + 'nghttp3/lib/nghttp3_wt.c', ], 'ngtcp2_test_server_sources': [ 'ngtcp2/examples/tls_server_session_ossl.cc', From 3652d64a7b4add5fb73772c34075d6d8b275391f Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 15 Aug 2026 06:59:21 +0200 Subject: [PATCH 03/37] Update nghttp3 with fix --- .../nghttp3/lib/includes/nghttp3/nghttp3.h | 14 ++--- .../nghttp3/lib/includes/nghttp3/version.h | 4 +- deps/ngtcp2/nghttp3/lib/nghttp3_conn.c | 6 +-- deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c | 15 +++--- deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c | 54 ++++++++++++------- deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h | 8 +-- deps/ngtcp2/nghttp3/lib/nghttp3_stream.h | 5 +- 7 files changed, 60 insertions(+), 46 deletions(-) diff --git a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h index 39ba6602c5a7..37d50c83925f 100644 --- a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h +++ b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h @@ -2354,11 +2354,11 @@ typedef int (*nghttp3_recv_settings2)(nghttp3_conn *conn, * invoked when a stream identified by |stream_id| is closed. If * :macro:`NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET` is set in * |flags|, |rx_app_error_code| is the QUIC application error code - * received from the remote endpoint. If + * that shut down the receiving side of the stream. If * :macro:`NGHTTP3_STREAM_CLOSE_FLAG_TX_APP_ERROR_CODE_SET` is set in * |flags|, |tx_app_error_code| is the QUIC application error code - * sent to the remote endpoint. No application code means that - * direction of stream is closed without any error. + * that shut down the sending side of the stream. No application code + * means that direction of stream is closed without any error. * * This callback should be used with `nghttp3_conn_close_stream2`. If * `nghttp3_conn_close_stream` is used, the app_error_code is set to @@ -3086,11 +3086,11 @@ NGHTTP3_EXTERN int nghttp3_conn_close_stream(nghttp3_conn *conn, * `nghttp3_conn_close_stream2` tells the library that a stream * identified by |stream_id| has been closed. If * :macro:`NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET` is set in - * |flags|, |rx_app_error_code| is QUIC application error received - * from the remote endpoint. Similarly, + * |flags|, |rx_app_error_code| is the QUIC application error code + * that shut down the receiving side of the stream. Similarly, * :macro:`NGHTTP3_STREAM_CLOSE_FLAG_TX_APP_ERROR_CODE_SET` is set in - * |flags|, |tx_app_error_code| is QUIC application error sent to the - * remote endpoint. + * |flags|, |tx_app_error_code| is the QUIC application error code + * that shut down the sending side of the stream. * * For stream close callback, prefer * :member:`nghttp3_callbacks.stream_close2` to diff --git a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h index bbe445ae0f44..4b87e9052f5a 100644 --- a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h +++ b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/version.h @@ -31,7 +31,7 @@ * * Version number of the nghttp3 library release. */ -#define NGHTTP3_VERSION "1.18.0-DEV" +#define NGHTTP3_VERSION "1.19.0-DEV" /** * @macro @@ -41,6 +41,6 @@ * number, 8 bits for minor and 8 bits for patch. Version 1.2.3 * becomes 0x010203. */ -#define NGHTTP3_VERSION_NUM 0x011200 +#define NGHTTP3_VERSION_NUM 0x011300 #endif /* !defined(NGHTTP3_VERSION_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c index ad007fc2f8a4..593d7e618b1b 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c @@ -1340,7 +1340,7 @@ nghttp3_ssize nghttp3_conn_read_control(nghttp3_conn *conn, for (; conn->rx.originlen_offset < sizeof(conn->rx.originlen) && (size_t)nread < len; ++conn->rx.originlen_offset, ++nread) { - conn->rx.originlen <<= 8; + conn->rx.originlen *= 256; conn->rx.originlen += *p++; } @@ -3086,7 +3086,7 @@ void nghttp3_conn_block_stream(nghttp3_conn *conn, int64_t stream_id) { stream->flags |= NGHTTP3_STREAM_FLAG_FC_BLOCKED; stream->unscheduled_nwrite = 0; - if (nghttp3_client_stream_bidi(stream->node.id)) { + if (nghttp3_stream_schedulable(stream)) { nghttp3_conn_unschedule_stream(conn, stream); } } @@ -3101,7 +3101,7 @@ void nghttp3_conn_shutdown_stream_write(nghttp3_conn *conn, int64_t stream_id) { stream->flags |= NGHTTP3_STREAM_FLAG_SHUT_WR; stream->unscheduled_nwrite = 0; - if (nghttp3_client_stream_bidi(stream->node.id)) { + if (nghttp3_stream_schedulable(stream)) { nghttp3_conn_unschedule_stream(conn, stream); } } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c b/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c index c1f3cdf9b937..4ab9b81fde0c 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_ksl.c @@ -226,22 +226,21 @@ static int ksl_split_node(nghttp3_ksl *ksl, nghttp3_ksl_blk *blk, size_t i) { * Out of memory. */ static int ksl_split_root(nghttp3_ksl *ksl) { - nghttp3_ksl_blk *rblk = NULL, *lblk, *nroot = NULL; + nghttp3_ksl_blk *rblk, *lblk, *nroot; + + nroot = ksl_blk_objalloc_new(ksl); + if (nroot == NULL) { + return NGHTTP3_ERR_NOMEM; + } rblk = ksl_split_blk(ksl, ksl->root); if (rblk == NULL) { + ksl_blk_objalloc_del(ksl, nroot); return NGHTTP3_ERR_NOMEM; } lblk = ksl->root; - nroot = ksl_blk_objalloc_new(ksl); - - if (nroot == NULL) { - ksl_blk_objalloc_del(ksl, rblk); - return NGHTTP3_ERR_NOMEM; - } - nroot->next = nroot->prev = NULL; nroot->n = 2; nroot->leaf = 0; diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c index 59b633ea0572..8e950a201d89 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c @@ -2820,6 +2820,7 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, int busy = 0; const nghttp3_mem *mem = decoder->ctx.mem; nghttp3_ssize nread; + size_t huff_declen; int rfin; if (decoder->ctx.bad) { @@ -2958,13 +2959,16 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, } if (decoder->rstate.huffman_encoded) { + huff_declen = nghttp3_qpack_huffman_estimate_decode_length( + (size_t)decoder->rstate.left); + if (huff_declen > NGHTTP3_QPACK_MAX_NAMELEN) { + rv = NGHTTP3_ERR_QPACK_HEADER_TOO_LARGE; + goto fail; + } + decoder->state = NGHTTP3_QPACK_ES_STATE_READ_NAME_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&decoder->rstate.huffman_ctx); - rv = nghttp3_rcbuf_new(&decoder->rstate.name, - nghttp3_qpack_huffman_estimate_decode_length( - (size_t)decoder->rstate.left) + - 1, - mem); + rv = nghttp3_rcbuf_new(&decoder->rstate.name, huff_declen + 1, mem); } else { decoder->state = NGHTTP3_QPACK_ES_STATE_READ_NAME; rv = nghttp3_rcbuf_new(&decoder->rstate.name, @@ -3043,13 +3047,16 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, } if (decoder->rstate.huffman_encoded) { + huff_declen = nghttp3_qpack_huffman_estimate_decode_length( + (size_t)decoder->rstate.left); + if (huff_declen > NGHTTP3_QPACK_MAX_VALUELEN) { + rv = NGHTTP3_ERR_QPACK_HEADER_TOO_LARGE; + goto fail; + } + decoder->state = NGHTTP3_QPACK_ES_STATE_READ_VALUE_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&decoder->rstate.huffman_ctx); - rv = nghttp3_rcbuf_new(&decoder->rstate.value, - nghttp3_qpack_huffman_estimate_decode_length( - (size_t)decoder->rstate.left) + - 1, - mem); + rv = nghttp3_rcbuf_new(&decoder->rstate.value, huff_declen + 1, mem); } else { decoder->state = NGHTTP3_QPACK_ES_STATE_READ_VALUE; rv = nghttp3_rcbuf_new(&decoder->rstate.value, @@ -3342,6 +3349,7 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, nghttp3_ssize nread; int rfin; const nghttp3_mem *mem = decoder->ctx.mem; + size_t huff_declen; if (decoder->ctx.bad) { return NGHTTP3_ERR_QPACK_FATAL; @@ -3565,13 +3573,16 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, } if (sctx->rstate.huffman_encoded) { + huff_declen = nghttp3_qpack_huffman_estimate_decode_length( + (size_t)sctx->rstate.left); + if (huff_declen > NGHTTP3_QPACK_MAX_NAMELEN) { + rv = NGHTTP3_ERR_QPACK_HEADER_TOO_LARGE; + goto fail; + } + sctx->state = NGHTTP3_QPACK_RS_STATE_READ_NAME_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&sctx->rstate.huffman_ctx); - rv = nghttp3_rcbuf_new(&sctx->rstate.name, - nghttp3_qpack_huffman_estimate_decode_length( - (size_t)sctx->rstate.left) + - 1, - mem); + rv = nghttp3_rcbuf_new(&sctx->rstate.name, huff_declen + 1, mem); } else { sctx->state = NGHTTP3_QPACK_RS_STATE_READ_NAME; rv = nghttp3_rcbuf_new(&sctx->rstate.name, @@ -3648,13 +3659,16 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, } if (sctx->rstate.huffman_encoded) { + huff_declen = nghttp3_qpack_huffman_estimate_decode_length( + (size_t)sctx->rstate.left); + if (huff_declen > NGHTTP3_QPACK_MAX_VALUELEN) { + rv = NGHTTP3_ERR_QPACK_HEADER_TOO_LARGE; + goto fail; + } + sctx->state = NGHTTP3_QPACK_RS_STATE_READ_VALUE_HUFFMAN; nghttp3_qpack_huffman_decode_context_init(&sctx->rstate.huffman_ctx); - rv = nghttp3_rcbuf_new(&sctx->rstate.value, - nghttp3_qpack_huffman_estimate_decode_length( - (size_t)sctx->rstate.left) + - 1, - mem); + rv = nghttp3_rcbuf_new(&sctx->rstate.value, huff_declen + 1, mem); } else { sctx->state = NGHTTP3_QPACK_RS_STATE_READ_VALUE; rv = nghttp3_rcbuf_new(&sctx->rstate.value, diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h index 2b8dca9d5cf7..934cae77c1bf 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.h @@ -42,11 +42,11 @@ #define NGHTTP3_QPACK_INT_MAX ((1ULL << 62) - 1) -/* NGHTTP3_QPACK_MAX_NAMELEN is the maximum (compressed) length of - header name this library can decode. */ +/* NGHTTP3_QPACK_MAX_NAMELEN is the maximum (estimated uncompressed) + length of header name this library can decode. */ #define NGHTTP3_QPACK_MAX_NAMELEN 256 -/* NGHTTP3_QPACK_MAX_VALUELEN is the maximum (compressed) length of - header value this library can decode. */ +/* NGHTTP3_QPACK_MAX_VALUELEN is the maximum (estimated uncompressed) + length of header value this library can decode. */ #define NGHTTP3_QPACK_MAX_VALUELEN 65536 /* NGHTTP3_QPACK_MAX_ENCODERLEN is the maximum encoder stream length that a decoder accepts without completely processing a single field diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h index 0ea948e81159..509a11cf0217 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_stream.h @@ -42,8 +42,9 @@ #define NGHTTP3_STREAM_MIN_CHUNK_SIZE 256 /* NGHTTP3_MIN_UNSENT_BYTES is the minimum unsent bytes which is large - enough to fill outgoing single QUIC packet. */ -#define NGHTTP3_MIN_UNSENT_BYTES 4096 + enough to fill outgoing single QUIC packet or TLS record in case of + QMux (Cut 2 bytes for QMux record length). */ +#define NGHTTP3_MIN_UNSENT_BYTES 16382 /* NGHTTP3_STREAM_MIN_WRITELEN is the minimum length of write to cause the stream to reschedule. */ From 7a7cb3769656381074bf401dd1d05311a4efebc1 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Mon, 31 Aug 2026 09:39:06 +0200 Subject: [PATCH 04/37] Update nghttp3 from upstream --- deps/ngtcp2/nghttp3/lib/nghttp3_conn.c | 63 +++++++++++++++++-------- deps/ngtcp2/nghttp3/lib/nghttp3_err.c | 5 ++ deps/ngtcp2/nghttp3/lib/nghttp3_err.h | 6 +++ deps/ngtcp2/nghttp3/lib/nghttp3_map.c | 7 ++- deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c | 6 +++ 5 files changed, 66 insertions(+), 21 deletions(-) diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c index 593d7e618b1b..24795e905a5c 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c @@ -682,13 +682,14 @@ nghttp3_ssize nghttp3_conn_read_stream2(nghttp3_conn *conn, int64_t stream_id, return 0; } + if (fin) { + stream->flags |= NGHTTP3_STREAM_FLAG_READ_EOF; + } + if (nghttp3_stream_uni(stream_id)) { return nghttp3_conn_read_uni(conn, stream, src, srclen, fin, ts); } - if (fin) { - stream->flags |= NGHTTP3_STREAM_FLAG_READ_EOF; - } return nghttp3_conn_read_bidi(conn, &bidi_nproc, stream, src, srclen, fin, ts); } @@ -1835,13 +1836,14 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, /* rstate->left is Session ID */ rv = nghttp3_conn_on_wt_stream(conn, stream, (int64_t)rstate->left); if (rv != 0) { - if (rv != NGHTTP3_ERR_WT_SESSION_GONE) { + if (!nghttp3_err_is_wt(rv)) { return rv; } stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; - rv = nghttp3_conn_abort_stream(conn, stream, NGHTTP3_WT_SESSION_GONE); + rv = nghttp3_conn_abort_stream( + conn, stream, nghttp3_err_infer_quic_app_error_code(rv)); if (rv != 0) { return rv; } @@ -1882,12 +1884,12 @@ nghttp3_ssize nghttp3_conn_read_bidi(nghttp3_conn *conn, size_t *pnproc, len = (size_t)nghttp3_min(rstate->left, (uint64_t)(end - p)); nread = nghttp3_conn_on_data(conn, stream, p, len); if (nread < 0) { - if (nread != NGHTTP3_ERR_WT_SESSION_GONE) { + if (!nghttp3_err_is_wt((int)nread)) { return nread; } - rv = nghttp3_conn_shutdown_wt_session(conn, stream, - NGHTTP3_WT_SESSION_GONE); + rv = nghttp3_conn_shutdown_wt_session( + conn, stream, nghttp3_err_infer_quic_app_error_code((int)nread)); if (rv != 0) { return rv; } @@ -2310,6 +2312,12 @@ int nghttp3_conn_on_settings_entry_received(nghttp3_conn *conn, break; } +#if SIZE_MAX < UINT64_MAX + if (ent->value > SIZE_MAX) { + return NGHTTP3_ERR_H3_SETTINGS_ERROR; + } +#endif /* SIZE_MAX < UINT64_MAX */ + dest->qpack_max_dtable_capacity = (size_t)ent->value; nghttp3_qpack_encoder_set_max_dtable_capacity(&conn->qenc, @@ -2324,6 +2332,12 @@ int nghttp3_conn_on_settings_entry_received(nghttp3_conn *conn, break; } +#if SIZE_MAX < UINT64_MAX + if (ent->value > SIZE_MAX) { + return NGHTTP3_ERR_H3_SETTINGS_ERROR; + } +#endif /* SIZE_MAX < UINT64_MAX */ + dest->qpack_blocked_streams = (size_t)ent->value; nghttp3_qpack_encoder_set_max_blocked_streams( @@ -3528,14 +3542,8 @@ int nghttp3_conn_server_confirm_wt_session(nghttp3_conn *conn, } int nghttp3_conn_open_wt_session(nghttp3_conn *conn, nghttp3_stream *stream) { - int rv; - - rv = nghttp3_wt_session_new(&stream->wt.session, stream->node.id, conn->mem); - if (rv != 0) { - return rv; - } - - return 0; + return nghttp3_wt_session_new(&stream->wt.session, stream->node.id, + conn->mem); } int nghttp3_conn_open_wt_data_stream(nghttp3_conn *conn, int64_t session_id, @@ -3687,7 +3695,7 @@ int nghttp3_conn_close_wt_session(nghttp3_conn *conn, int64_t session_id, return NGHTTP3_ERR_STREAM_NOT_FOUND; } - if (!nghttp3_stream_wt_ctrl(stream)) { + if (!nghttp3_stream_wt_ctrl(stream) || msglen > 1024) { return NGHTTP3_ERR_INVALID_ARGUMENT; } @@ -3765,7 +3773,21 @@ int nghttp3_conn_on_wt_stream(nghttp3_conn *conn, nghttp3_stream *stream, } wt_ctrl_stream = nghttp3_conn_find_stream(conn, session_id); - if (!wt_ctrl_stream) { + if (wt_ctrl_stream) { + if (conn->server) { + if ((wt_ctrl_stream->flags & NGHTTP3_STREAM_FLAG_RESP_SUBMITTED) && + !wt_ctrl_stream->wt.session) { + /* Server has submitted the regular non-WebTransport response. + wt_ctrl_stream is not WebTransport session stream. */ + return NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; + } + } else if (!wt_ctrl_stream->wt.session) { + /* On client side, if it has not submitted the request with + nghttp3_conn_submit_wt_request, it is not WebTransport + session stream. */ + return NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; + } + } else { if (!conn->server) { /* On client's perspective, if session stream is not found, we are sure that session is gone. */ @@ -3906,13 +3928,14 @@ nghttp3_ssize nghttp3_conn_read_wt_stream_uni(nghttp3_conn *conn, /* rstate->left is Session ID */ rv = nghttp3_conn_on_wt_stream(conn, stream, (int64_t)rstate->left); if (rv != 0) { - if (rv != NGHTTP3_ERR_WT_SESSION_GONE) { + if (!nghttp3_err_is_wt(rv)) { return rv; } stream->rstate.state = NGHTTP3_WT_STREAM_STATE_IGN_REST; - rv = nghttp3_conn_abort_stream(conn, stream, NGHTTP3_WT_SESSION_GONE); + rv = nghttp3_conn_abort_stream(conn, stream, + nghttp3_err_infer_quic_app_error_code(rv)); if (rv != 0) { return rv; } diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_err.c b/deps/ngtcp2/nghttp3/lib/nghttp3_err.c index e6603c00f041..c770fd4cbd1f 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_err.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_err.c @@ -138,3 +138,8 @@ uint64_t nghttp3_err_infer_quic_app_error_code(int liberr) { } int nghttp3_err_is_fatal(int liberr) { return liberr < NGHTTP3_ERR_FATAL; } + +int nghttp3_err_is_wt(int liberr) { + return liberr == NGHTTP3_ERR_WT_SESSION_GONE || + liberr == NGHTTP3_ERR_WT_BUFFERED_STREAM_REJECTED; +} diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_err.h b/deps/ngtcp2/nghttp3/lib/nghttp3_err.h index 6f8205cc17ce..8dbc234e0aec 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_err.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_err.h @@ -31,4 +31,10 @@ #include +/* + * nghttp3_err_wt returns nonzero if |liberr| is one of WebTransport + * errors. + */ +int nghttp3_err_is_wt(int liberr); + #endif /* !defined(NGHTTP3_ERR_H) */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_map.c b/deps/ngtcp2/nghttp3/lib/nghttp3_map.c index 7858d4cc3eb1..0a34be7bba13 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_map.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_map.c @@ -275,7 +275,12 @@ int nghttp3_map_insert(nghttp3_map *map, nghttp3_map_key_type key, void *data) { return 0; } - return map_resize(map, map->hashbits + 1); + rv = map_resize(map, map->hashbits + 1); + if (rv != 0) { + nghttp3_map_remove(map, key); + } + + return rv; } void *nghttp3_map_find(const nghttp3_map *map, nghttp3_map_key_type key) { diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c index 8e950a201d89..7b244a4d1b6e 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c @@ -2895,6 +2895,12 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, if (decoder->opcode == NGHTTP3_QPACK_ES_OPCODE_SET_DTABLE_CAP) { DEBUGF("qpack::decode: Set dtable capacity to %" PRIu64 "\n", decoder->rstate.left); +#if SIZE_MAX < UINT64_MAX + if (decoder->rstate.left > SIZE_MAX) { + return NGHTTP3_ERR_QPACK_ENCODER_STREAM_ERROR; + } +#endif /* SIZE_MAX < UINT64_MAX */ + rv = nghttp3_qpack_decoder_set_max_dtable_capacity( decoder, (size_t)decoder->rstate.left); if (rv != 0) { From 24d78b5a6e853a4a37a96d689c229b9602db3751 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 26 Sep 2026 08:41:31 +0200 Subject: [PATCH 05/37] Update of nghttp3 webtransport --- .../nghttp3/lib/includes/nghttp3/nghttp3.h | 13 +- deps/ngtcp2/nghttp3/lib/nghttp3_conn.c | 2 + deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c | 117 ++++++++---------- deps/ngtcp2/nghttp3/lib/nghttp3_ratelim.c | 19 ++- deps/ngtcp2/nghttp3/lib/nghttp3_ratelim.h | 7 +- deps/ngtcp2/nghttp3/lib/nghttp3_unreachable.h | 6 +- 6 files changed, 79 insertions(+), 85 deletions(-) diff --git a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h index 37d50c83925f..424be3ffb557 100644 --- a/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h +++ b/deps/ngtcp2/nghttp3/lib/includes/nghttp3/nghttp3.h @@ -1924,12 +1924,13 @@ typedef struct nghttp3_settings { NGHTTP3_SETTINGS_V3. */ /** * :member:`glitch_ratelim_burst` is the maximum number of tokens - * available to "glitch" rate limiter. "glitch" is a suspicious - * activity from a remote endpoint. If detected, certain amount of - * tokens are consumed. If no tokens are available to consume, the - * connection is closed. The rate of token generation is specified - * by :member:`glitch_ratelim_rate`. This feature is enabled only - * when `nghttp3_conn_read_stream2` is used. + * available to "glitch" rate limiter. It is clamped to UINT64_MAX + * / NGHTTP3_SECONDS. "glitch" is a suspicious activity from a + * remote endpoint. If detected, certain amount of tokens are + * consumed. If no tokens are available to consume, the connection + * is closed. The rate of token generation is specified by + * :member:`glitch_ratelim_rate`. This feature is enabled only when + * `nghttp3_conn_read_stream2` is used. * * .. version-added:: 1.12.0 */ diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c index 24795e905a5c..e8df764a16d5 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c @@ -3387,6 +3387,8 @@ int nghttp3_conn_set_client_stream_priority(nghttp3_conn *conn, memcpy(buf, data, datalen); } + assert(conn->tx.ctrl); + rv = nghttp3_stream_frq_emplace(conn->tx.ctrl, &fr); if (rv != 0) { nghttp3_mem_free(conn->mem, buf); diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c index 7b244a4d1b6e..2e3d063e1381 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_qpack.c @@ -2568,24 +2568,23 @@ nghttp3_ssize nghttp3_qpack_encoder_read_decoder(nghttp3_qpack_encoder *encoder, for (; p != end;) { switch (encoder->state) { case NGHTTP3_QPACK_DS_STATE_OPCODE: - switch ((*p) & 0xC0U) { - case 0x80U: - case 0xC0U: - DEBUGF("qpack::encode: OPCODE_SECTION_ACK\n"); - encoder->opcode = NGHTTP3_QPACK_DS_OPCODE_SECTION_ACK; - encoder->rstate.prefix = 7; + switch ((*p) >> 6) { + case 0x0U: + DEBUGF("qpack::encode: OPCODE_ICNT_INCREMENT\n"); + encoder->opcode = NGHTTP3_QPACK_DS_OPCODE_ICNT_INCREMENT; + encoder->rstate.prefix = 6; break; - case 0x40U: + case 0x1U: DEBUGF("qpack::encode: OPCODE_STREAM_CANCEL\n"); encoder->opcode = NGHTTP3_QPACK_DS_OPCODE_STREAM_CANCEL; encoder->rstate.prefix = 6; break; default: - DEBUGF("qpack::encode: OPCODE_ICNT_INCREMENT\n"); - encoder->opcode = NGHTTP3_QPACK_DS_OPCODE_ICNT_INCREMENT; - encoder->rstate.prefix = 6; + DEBUGF("qpack::encode: OPCODE_SECTION_ACK\n"); + encoder->opcode = NGHTTP3_QPACK_DS_OPCODE_SECTION_ACK; + encoder->rstate.prefix = 7; } encoder->state = NGHTTP3_QPACK_DS_STATE_READ_NUMBER; /* fall through */ @@ -2842,39 +2841,36 @@ nghttp3_ssize nghttp3_qpack_decoder_read_encoder(nghttp3_qpack_decoder *decoder, busy = 0; switch (decoder->state) { case NGHTTP3_QPACK_ES_STATE_OPCODE: - switch ((*p) & 0xE0U) { - case 0x80U: - case 0xA0U: - case 0xC0U: - case 0xE0U: - DEBUGF("qpack::decode: OPCODE_INSERT_INDEXED\n"); - decoder->opcode = NGHTTP3_QPACK_ES_OPCODE_INSERT_INDEXED; - decoder->rstate.dynamic = !((*p) & 0x40U); - decoder->rstate.prefix = 6; + switch ((*p) >> 5) { + case 0x0U: + DEBUGF("qpack::decode: OPCODE_DUPLICATE\n"); + decoder->opcode = NGHTTP3_QPACK_ES_OPCODE_DUPLICATE; + decoder->rstate.dynamic = 1; + decoder->rstate.prefix = 5; decoder->state = NGHTTP3_QPACK_ES_STATE_READ_INDEX; break; - case 0x40U: - case 0x60U: + case 0x1U: + DEBUGF("qpack::decode: OPCODE_SET_DTABLE_TABLE_CAP\n"); + decoder->opcode = NGHTTP3_QPACK_ES_OPCODE_SET_DTABLE_CAP; + decoder->rstate.prefix = 5; + decoder->state = NGHTTP3_QPACK_ES_STATE_READ_INDEX; + + break; + case 0x2U: + case 0x3U: DEBUGF("qpack::decode: OPCODE_INSERT\n"); decoder->opcode = NGHTTP3_QPACK_ES_OPCODE_INSERT; decoder->rstate.dynamic = 0; decoder->rstate.prefix = 5; decoder->state = NGHTTP3_QPACK_ES_STATE_CHECK_NAME_HUFFMAN; - break; - case 0x20U: - DEBUGF("qpack::decode: OPCODE_SET_DTABLE_TABLE_CAP\n"); - decoder->opcode = NGHTTP3_QPACK_ES_OPCODE_SET_DTABLE_CAP; - decoder->rstate.prefix = 5; - decoder->state = NGHTTP3_QPACK_ES_STATE_READ_INDEX; - break; default: - DEBUGF("qpack::decode: OPCODE_DUPLICATE\n"); - decoder->opcode = NGHTTP3_QPACK_ES_OPCODE_DUPLICATE; - decoder->rstate.dynamic = 1; - decoder->rstate.prefix = 5; + DEBUGF("qpack::decode: OPCODE_INSERT_INDEXED\n"); + decoder->opcode = NGHTTP3_QPACK_ES_OPCODE_INSERT_INDEXED; + decoder->rstate.dynamic = !((*p) & 0x40U); + decoder->rstate.prefix = 6; decoder->state = NGHTTP3_QPACK_ES_STATE_READ_INDEX; } break; @@ -3439,36 +3435,26 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, case NGHTTP3_QPACK_RS_STATE_OPCODE: assert(sctx->rstate.left == 0); assert(sctx->rstate.shift == 0); - switch ((*p) & 0xF0U) { - case 0x80U: - case 0x90U: - case 0xA0U: - case 0xB0U: - case 0xC0U: - case 0xD0U: - case 0xE0U: - case 0xF0U: - DEBUGF("qpack::decode: OPCODE_INDEXED\n"); - sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED; - sctx->rstate.dynamic = !((*p) & 0x40U); - sctx->rstate.prefix = 6; + switch ((*p) >> 4) { + case 0x0U: + DEBUGF("qpack::decode: OPCODE_INDEXED_NAME_PB\n"); + sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED_NAME_PB; + sctx->rstate.never = (*p) & 0x08U; + sctx->rstate.dynamic = 1; + sctx->rstate.prefix = 3; sctx->state = NGHTTP3_QPACK_RS_STATE_READ_INDEX; break; - case 0x40U: - case 0x50U: - case 0x60U: - case 0x70U: - DEBUGF("qpack::decode: OPCODE_INDEXED_NAME\n"); - sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED_NAME; - sctx->rstate.never = (*p) & 0x20U; - sctx->rstate.dynamic = !((*p) & 0x10U); + case 0x1U: + DEBUGF("qpack::decode: OPCODE_INDEXED_PB\n"); + sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED_PB; + sctx->rstate.dynamic = 1; sctx->rstate.prefix = 4; sctx->state = NGHTTP3_QPACK_RS_STATE_READ_INDEX; break; - case 0x20U: - case 0x30U: + case 0x2U: + case 0x3U: DEBUGF("qpack::decode: OPCODE_LITERAL\n"); sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_LITERAL; sctx->rstate.never = (*p) & 0x10U; @@ -3477,20 +3463,23 @@ nghttp3_qpack_decoder_read_request(nghttp3_qpack_decoder *decoder, sctx->state = NGHTTP3_QPACK_RS_STATE_CHECK_NAME_HUFFMAN; break; - case 0x10U: - DEBUGF("qpack::decode: OPCODE_INDEXED_PB\n"); - sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED_PB; - sctx->rstate.dynamic = 1; + case 0x4U: + case 0x5U: + case 0x6U: + case 0x7U: + DEBUGF("qpack::decode: OPCODE_INDEXED_NAME\n"); + sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED_NAME; + sctx->rstate.never = (*p) & 0x20U; + sctx->rstate.dynamic = !((*p) & 0x10U); sctx->rstate.prefix = 4; sctx->state = NGHTTP3_QPACK_RS_STATE_READ_INDEX; break; default: - DEBUGF("qpack::decode: OPCODE_INDEXED_NAME_PB\n"); - sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED_NAME_PB; - sctx->rstate.never = (*p) & 0x08U; - sctx->rstate.dynamic = 1; - sctx->rstate.prefix = 3; + DEBUGF("qpack::decode: OPCODE_INDEXED\n"); + sctx->opcode = NGHTTP3_QPACK_RS_OPCODE_INDEXED; + sctx->rstate.dynamic = !((*p) & 0x40U); + sctx->rstate.prefix = 6; sctx->state = NGHTTP3_QPACK_RS_STATE_READ_INDEX; } break; diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_ratelim.c b/deps/ngtcp2/nghttp3/lib/nghttp3_ratelim.c index d7a0267530ca..d6d0738e5de6 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_ratelim.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_ratelim.c @@ -32,6 +32,8 @@ void nghttp3_ratelim_init(nghttp3_ratelim *rlim, uint64_t burst, uint64_t rate, nghttp3_tstamp ts) { + burst = nghttp3_min(burst, NGHTTP3_RATELIM_MAX_BURST); + *rlim = (nghttp3_ratelim){ .burst = burst, .rate = rate, @@ -53,19 +55,16 @@ static void ratelim_update(nghttp3_ratelim *rlim, nghttp3_tstamp ts) { d = ts - rlim->ts; rlim->ts = ts; - if (rlim->rate > (UINT64_MAX - rlim->carry) / d) { - gain = UINT64_MAX; - } else { + if (rlim->rate <= (UINT64_MAX - rlim->carry) / d) { gain = rlim->rate * d + rlim->carry; - } + gps = gain / NGHTTP3_SECONDS; - gps = gain / NGHTTP3_SECONDS; + if (gps < rlim->burst && rlim->tokens < rlim->burst - gps) { + rlim->tokens += gps; + rlim->carry = gain % NGHTTP3_SECONDS; - if (gps < rlim->burst && rlim->tokens < rlim->burst - gps) { - rlim->tokens += gps; - rlim->carry = gain % NGHTTP3_SECONDS; - - return; + return; + } } rlim->tokens = rlim->burst; diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_ratelim.h b/deps/ngtcp2/nghttp3/lib/nghttp3_ratelim.h index 68aab93a61bf..ba8f53788146 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_ratelim.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_ratelim.h @@ -33,6 +33,9 @@ #include +/* NGHTTP3_RATELIM_MAX_BURST is the maximum value of the burst. */ +#define NGHTTP3_RATELIM_MAX_BURST (UINT64_MAX / NGHTTP3_SECONDS) + typedef struct nghttp3_ratelim { /* burst is the maximum number of tokens. */ uint64_t burst; @@ -48,8 +51,8 @@ typedef struct nghttp3_ratelim { nghttp3_tstamp ts; } nghttp3_ratelim; -/* nghttp3_ratelim_init initializes |rlim| with the given - parameters. */ +/* nghttp3_ratelim_init initializes |rlim| with the given parameters. + |burst| is clamped to NGHTTP3_RATELIM_MAX_BURST. */ void nghttp3_ratelim_init(nghttp3_ratelim *rlim, uint64_t burst, uint64_t rate, nghttp3_tstamp ts); diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_unreachable.h b/deps/ngtcp2/nghttp3/lib/nghttp3_unreachable.h index c609d7ed72f3..362ceb08c3d2 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_unreachable.h +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_unreachable.h @@ -44,10 +44,10 @@ #ifdef _MSC_VER __declspec(noreturn) #endif /* defined(_MSC_VER) */ - void nghttp3_unreachable_fail(const char *file, int line, const char *func) +void nghttp3_unreachable_fail(const char *file, int line, const char *func) #ifndef _MSC_VER - __attribute__((noreturn)) + __attribute__((noreturn)) #endif /* !defined(_MSC_VER) */ - ; + ; #endif /* !defined(NGHTTP3_UNREACHABLE_H) */ From 9329dd83a8de4d8ef26ab6ad6365e06cb47c9473 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 23 May 2026 18:02:24 +0200 Subject: [PATCH 06/37] quic: Implement webtransport settings --- lib/internal/quic/quic.js | 1 + src/quic/application.cc | 7 +++++- src/quic/bindingdata.h | 1 + src/quic/http3.cc | 22 ++++++++++++------- src/quic/session.h | 1 + test/parallel/test-quic-h3-settings.mjs | 1 + .../test-quic-session-application-options.mjs | 2 ++ 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 4922ce562751..9d3f68f44223 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -372,6 +372,7 @@ const endpointRegistry = new SafeSet(); * @property {bigint|number} [qpackBlockedStreams] The qpack blocked streams * @property {boolean} [enableConnectProtocol] Enable the connect protocol * @property {boolean} [enableDatagrams] Enable datagrams + * @property {boolean} [enableWebtransport] Enable webtransport */ /** diff --git a/src/quic/application.cc b/src/quic/application.cc index 6f25c3771a75..0530118edd4c 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -57,6 +57,7 @@ Session::Application_Options::operator const nghttp3_settings() const { .glitch_ratelim_burst = 1000, .glitch_ratelim_rate = 33, .qpack_indexing_strat = NGHTTP3_QPACK_INDEXING_STRAT_EAGER, + .wt_enabled = enable_webtransport }; } @@ -78,6 +79,8 @@ std::string Session::Application_Options::ToString() const { (enable_connect_protocol ? std::string("yes") : std::string("no")); res += prefix + "enable datagrams: " + (enable_datagrams ? std::string("yes") : std::string("no")); + res += prefix + "webtransport enabled: " + + (enable_webtransport ? std::string("yes") : std::string("no")); res += indent.Close(); return res; } @@ -107,7 +110,7 @@ Maybe Session::Application_Options::From( !SET(max_field_section_size) || !SET(qpack_max_dtable_capacity) || !SET(qpack_encoder_max_dtable_capacity) || !SET(qpack_blocked_streams) || !SET(enable_connect_protocol) || - !SET(enable_datagrams)) { + !SET(enable_datagrams) || !SET(enable_webtransport)) { // The call to SetOption should have scheduled an exception to be thrown. return Nothing(); } @@ -138,6 +141,7 @@ MaybeLocal Session::Application_Options::ToObject( "qpackBlockedStreams", "enableConnectProtocol", "enableDatagrams", + "enableWebtransport" }; if (tmpl.IsEmpty()) { tmpl = DictionaryTemplate::New(env->isolate(), names); @@ -153,6 +157,7 @@ MaybeLocal Session::Application_Options::ToObject( BigInt::NewFromUnsigned(env->isolate(), qpack_blocked_streams), Boolean::New(env->isolate(), enable_connect_protocol), Boolean::New(env->isolate(), enable_datagrams), + Boolean::New(env->isolate(), enable_webtransport), }; static_assert(std::size(values) == std::size(names)); diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 2ef9f7685314..a5fe6f549cb8 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -92,6 +92,7 @@ class SessionManager; V(enable_connect_protocol, "enableConnectProtocol") \ V(enable_early_data, "enableEarlyData") \ V(enable_datagrams, "enableDatagrams") \ + V(enable_webtransport, "enableWebtransport") \ V(enable_tls_trace, "tlsTrace") \ V(endpoint, "Endpoint") \ V(endpoint_udp, "Endpoint::UDP") \ diff --git a/src/quic/http3.cc b/src/quic/http3.cc index fb48bd3e0032..5d3084a395a5 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -32,8 +32,8 @@ namespace quic { namespace { constexpr uint8_t kSessionTicketAppDataVersion = 1; -// Layout: [type(1)][version(1)][crc(4)][payload(34)] = 40 bytes -constexpr size_t kSessionTicketAppDataSize = 40; +// Layout: [type(1)][version(1)][crc(4)][payload(35)] = 41 bytes +constexpr size_t kSessionTicketAppDataSize = 41; constexpr size_t kSessionTicketAppDataHeaderSize = 6; // type + version + crc constexpr size_t kSessionTicketAppDataPayloadSize = kSessionTicketAppDataSize - kSessionTicketAppDataHeaderSize; @@ -391,8 +391,9 @@ class Http3ApplicationImpl final : public Session::Application { 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[32] = options_.enable_connect_protocol ? 1 : 0; // May be bitfield should be used! payload[33] = options_.enable_datagrams ? 1 : 0; + payload[34] = options_.enable_webtransport ? 1 : 0; uLong crc = crc32(0L, Z_NULL, 0); crc = crc32(crc, payload, kSessionTicketAppDataPayloadSize); @@ -445,32 +446,36 @@ class Http3ApplicationImpl final : public Session::Application { uint64_t stored_qpack_blocked_streams = ReadBE64(payload + 24); bool stored_enable_connect_protocol = payload[32] != 0; bool stored_enable_datagrams = payload[33] != 0; + bool stored_enable_webtransport = payload[34] != 0; Debug(&session(), "Ticket app data: stored mfss=%" PRIu64 " qmdc=%" PRIu64 - " qemdc=%" PRIu64 " qbs=%" PRIu64 " ecp=%d ed=%d", + " qemdc=%" PRIu64 " qbs=%" PRIu64 " ecp=%d ed=%d ew=%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); + stored_enable_datagrams, + stored_enable_webtransport); Debug(&session(), "Current opts: mfss=%" PRIu64 " qmdc=%" PRIu64 " qemdc=%" PRIu64 - " qbs=%" PRIu64 " ecp=%d ed=%d", + " qbs=%" PRIu64 " ecp=%d ed=%d ew %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); + options_.enable_datagrams, + options_.enable_webtransport); 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)) { + (stored_enable_datagrams && !options_.enable_datagrams) || + (stored_enable_webtransport && !options_.enable_webtransport)) { Debug(&session(), "Ticket app data REJECTED"); return SessionTicket::AppData::Status::TICKET_IGNORE_RENEW; } @@ -1080,6 +1085,7 @@ class Http3ApplicationImpl final : public Session::Application { void OnReceiveSettings(const nghttp3_proto_settings* settings) { options_.enable_connect_protocol = settings->enable_connect_protocol; options_.enable_datagrams = settings->h3_datagram; + options_.enable_webtransport = settings->wt_enabled; 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; diff --git a/src/quic/session.h b/src/quic/session.h index 9834aa7ec130..2b6c4258df04 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -84,6 +84,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool enable_connect_protocol = true; bool enable_datagrams = true; + bool enable_webtransport = false; // for a client always enabling it, may be good operator const nghttp3_settings() const; diff --git a/test/parallel/test-quic-h3-settings.mjs b/test/parallel/test-quic-h3-settings.mjs index 733d3865e872..01b3e9edf9fe 100644 --- a/test/parallel/test-quic-h3-settings.mjs +++ b/test/parallel/test-quic-h3-settings.mjs @@ -5,6 +5,7 @@ // maxHeaderLength enforcement - reject headers exceeding byte length // enableConnectProtocol setting (accepted without error) // enableDatagrams setting (accepted without error) +// enableWebtransport setting (accepted without error) import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; diff --git a/test/parallel/test-quic-session-application-options.mjs b/test/parallel/test-quic-session-application-options.mjs index b119207bcc1f..5a4bf9c460dd 100644 --- a/test/parallel/test-quic-session-application-options.mjs +++ b/test/parallel/test-quic-session-application-options.mjs @@ -23,6 +23,7 @@ const customAppOptions = { qpackBlockedStreams: 50n, enableConnectProtocol: false, enableDatagrams: false, + enableWebtransport: false, }; const serverDone = Promise.withResolvers(); @@ -51,6 +52,7 @@ const serverEndpoint = await listen(mustCall((serverSession) => { assert.strictEqual(opts.enableConnectProtocol, customAppOptions.enableConnectProtocol); assert.strictEqual(opts.enableDatagrams, customAppOptions.enableDatagrams); + assert.strictEqual(opts.enableWebtransport, customAppOptions.enableWebtransport); stream.writer.endSync(); await stream.closed; From a46a7532aa7599c1de3aa7210ca74d0e1d2a803a Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 24 May 2026 11:54:50 +0200 Subject: [PATCH 07/37] quic: Implement webtransport response and reply --- lib/internal/quic/quic.js | 33 +++++++++++++++++--- src/quic/application.h | 1 + src/quic/bindingdata.cc | 3 ++ src/quic/http3.cc | 63 ++++++++++++++++++++++++++++----------- src/quic/session.h | 3 +- 5 files changed, 81 insertions(+), 22 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 9d3f68f44223..16187045546a 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -78,6 +78,7 @@ const { QUIC_STREAM_HEADERS_KIND_TRAILING: kHeadersKindTrailing, QUIC_STREAM_HEADERS_FLAGS_NONE: kHeadersFlagsNone, QUIC_STREAM_HEADERS_FLAGS_TERMINAL: kHeadersFlagsTerminal, + QUIC_STREAM_HEADERS_FLAGS_WEBTRANSPORT: kHeadersFlagsWebtransport, } = internalBinding('quic'); // Maps the numeric HeadersKind constants from C++ to user-facing strings. @@ -294,7 +295,9 @@ const endpointRegistry = new SafeSet(); * @property {string|ArrayBuffer|SharedArrayBuffer|ArrayBufferView|Blob| * FileHandle|AsyncIterable|Iterable|Promise|null} [body] The outbound * body source. See the public docs for `stream.setBody()` for details - * on supported types. When omitted, the stream is closed immediately. + * on supported types. When omitted, the stream is closed immediately, + * except if the stream is a webtransport stream. For a webtransport + * stream setting body is illegal. * @property {object} [headers] Initial request or response headers to * send. Only used when the negotiated application supports headers * (e.g. HTTP/3). @@ -302,6 +305,8 @@ const endpointRegistry = new SafeSet(); * @property {boolean} [incremental] Whether to interleave data with same-priority streams. * @property {number} [budget] The byte budget for write backpressure. * **Default:** `65536`. + * @property {boolean} [webtransport] Indicates that the send headers signal + * webtransport support. If no headers are provided, it has no effect. * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers * @property {OnTrailersCallback} [ontrailers] Callback for incoming trailing headers * @property {OnInfoCallback} [oninfo] Callback for informational (1xx) headers @@ -516,6 +521,10 @@ const endpointRegistry = new SafeSet(); * @typedef {object} SendHeadersOptions * @property {boolean} [terminal] When true, indicates that no body data will be * sent after these headers. + * @property {boolean} [webtransport] When true, indicates that this is a header + * for a webtransport connection. Note, on the response, if you deny a + * webtransport connection, set it to false. A webtransport header can not be + * terminal. So we throw an error, if webtransport and terminal are both true. */ /** @@ -2158,10 +2167,15 @@ class QuicStream { 'The negotiated QUIC application protocol does not support headers'); } validateObject(headers, 'headers'); - const { terminal = false } = options; + const { terminal = false, webtransport = false } = options; + if (terminal && webtransport) { + throw new ERR_INVALID_ARG_VALUE( + 'webtransport and terminal can not be set simultaenously.'); + } const headerString = buildNgHeaderString( headers, assertValidPseudoHeader, true /* strictSingleValueFields */); - const flags = terminal ? kHeadersFlagsTerminal : kHeadersFlagsNone; + const flags = terminal ? kHeadersFlagsTerminal : + (webtransport ? kHeadersFlagsWebtransport : kHeadersFlagsNone); return sendHeaders( this.#handle, kHeadersKindInitial, headerString, flags); } @@ -3509,6 +3523,7 @@ class QuicSession { priority = 'default', incremental = false, budget = kDefaultBudget, + webtransport = false, headers, onheaders, ontrailers, @@ -3518,9 +3533,14 @@ class QuicSession { validateOneOf(priority, 'options.priority', ['default', 'low', 'high']); validateBoolean(incremental, 'options.incremental'); + validateBoolean(webtransport, 'options.webtransport'); const validatedBody = validateBody(body); + if (typeof body !== 'undefined' && webtransport) { + throw new ERR_INVALID_ARG_TYPE('options.body', 'A body can not be provided in webtransport mode'); + } + const handle = this.#handle.openStream(direction, validatedBody); if (handle === undefined) { throw new ERR_QUIC_OPEN_STREAM_FAILED(); @@ -3555,7 +3575,12 @@ class QuicSession { if (onwanttrailers) stream.onwanttrailers = onwanttrailers; if (headers !== undefined) { - stream.sendHeaders(headers, { terminal: validatedBody === undefined }); + stream.sendHeaders(headers, + { terminal: validatedBody === undefined && !webtransport, + webtransport }); + } else if (webtransport) { + throw new ERR_INVALID_ARG_VALUE('options.webtransport', + 'Specifying webtransport without a header has no effect'); } if (onSessionOpenStreamChannel.hasSubscribers) { diff --git a/src/quic/application.h b/src/quic/application.h index 4fd748763742..9d207eabd4f1 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -20,6 +20,7 @@ enum class HeadersKind : uint8_t { enum class HeadersFlags : uint8_t { NONE, TERMINAL, + WEBTRANSPORT, }; // An Application implements the ALPN-protocol specific semantics on behalf diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index e73ae82e980d..b8460beea07d 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -305,12 +305,15 @@ void BindingData::InitPerContext(Realm* realm, Local target) { static_cast(HeadersFlags::NONE); constexpr int QUIC_STREAM_HEADERS_FLAGS_TERMINAL = static_cast(HeadersFlags::TERMINAL); + constexpr int QUIC_STREAM_HEADERS_FLAGS_WEBTRANSPORT = + static_cast(HeadersFlags::WEBTRANSPORT); NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_HINTS); NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_INITIAL); NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_KIND_TRAILING); NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_FLAGS_NONE); NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_FLAGS_TERMINAL); + NODE_DEFINE_CONSTANT(target, QUIC_STREAM_HEADERS_FLAGS_WEBTRANSPORT); Realm::GetCurrent(realm->context())->AddBindingData(target); } diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 5d3084a395a5..4fbc03d58863 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -391,7 +391,8 @@ class Http3ApplicationImpl final : public Session::Application { 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; // May be bitfield should be used! + payload[32] = options_.enable_connect_protocol ? 1 : 0; + // May be bitfield should be used! payload[33] = options_.enable_datagrams ? 1 : 0; payload[34] = options_.enable_webtransport ? 1 : 0; @@ -736,30 +737,58 @@ class Http3ApplicationImpl final : public Session::Application { 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 (flags != HeadersFlags::TERMINAL + && flags != HeadersFlags::WEBTRANSPORT) { + reader_ptr = &reader; + } if (session().is_server()) { + if (flags != HeadersFlags::WEBTRANSPORT) { + 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 " response headers for stream %" PRIu64, + "Submitting %" PRIu64 " wt resp. headers for stream %" PRIu64, nva.length(), stream.id()); - return nghttp3_conn_submit_response(*this, + if (nghttp3_conn_submit_wt_response(*this, stream.id(), nva.data(), - nva.length(), - reader_ptr) == 0; + nva.length()) != 0) + return false; + return nghttp3_conn_server_confirm_wt_session(*this, + stream.id(), + 0) == 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; + if (flags != HeadersFlags::WEBTRANSPORT) { + 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; + } + Debug(&session(), + "Submitting %" PRIu64 " wt req. headers for stream %" PRIu64, + nva.length(), + stream.id()); + return nghttp3_conn_submit_wt_request(*this, + stream.id(), + nva.data(), + nva.length(), + const_cast(&stream)) + == 0; } case HeadersKind::TRAILING: { Debug(&session(), diff --git a/src/quic/session.h b/src/quic/session.h index 2b6c4258df04..3ab3503b2dd4 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -84,7 +84,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool enable_connect_protocol = true; bool enable_datagrams = true; - bool enable_webtransport = false; // for a client always enabling it, may be good + // for a client always enabling wt, may be good + bool enable_webtransport = false; operator const nghttp3_settings() const; From aaf362b5fec5c3ab14a4e138fa130bf431e4be76 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Mon, 25 May 2026 09:16:35 +0200 Subject: [PATCH 08/37] quic: Improve webtransport options and lint --- lib/internal/quic/quic.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 16187045546a..413313e47e5b 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -305,7 +305,7 @@ const endpointRegistry = new SafeSet(); * @property {boolean} [incremental] Whether to interleave data with same-priority streams. * @property {number} [budget] The byte budget for write backpressure. * **Default:** `65536`. - * @property {boolean} [webtransport] Indicates that the send headers signal + * @property {boolean} [webtransport] Indicates that the headers should send signal * webtransport support. If no headers are provided, it has no effect. * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers * @property {OnTrailersCallback} [ontrailers] Callback for incoming trailing headers @@ -2170,7 +2170,8 @@ class QuicStream { const { terminal = false, webtransport = false } = options; if (terminal && webtransport) { throw new ERR_INVALID_ARG_VALUE( - 'webtransport and terminal can not be set simultaenously.'); + 'webtransport and terminal can not be set simultaenously.', + { terminal, webtransport }); } const headerString = buildNgHeaderString( headers, assertValidPseudoHeader, true /* strictSingleValueFields */); From 1005dea25015c4ea026ed44e85523f71f7c6e67f Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 30 May 2026 19:44:12 +0200 Subject: [PATCH 09/37] quic: Make a webtransport stream --- lib/internal/quic/quic.js | 41 ++++++++++++++++++++++++++ lib/internal/quic/symbols.js | 2 ++ src/quic/application.h | 7 +++++ src/quic/bindingdata.cc | 22 ++++++++++++++ src/quic/bindingdata.h | 1 + src/quic/http3.cc | 57 +++++++++++++++++++++++++++++------- 6 files changed, 119 insertions(+), 11 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 413313e47e5b..13713d7a0d06 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -52,6 +52,7 @@ const { sendHeaders, setCallbacks, setHeadersInterest, + makeWebtransportStream, // The constants to be exposed to end users for various options. CC_ALGO_RENO_STR: CC_ALGO_RENO, @@ -211,6 +212,7 @@ const { kPrivateConstructor, kReset, kSendHeaders, + kMakeWebtransportStream, kSessionApplication, kSessionTicket, kStopSending, @@ -305,6 +307,7 @@ const endpointRegistry = new SafeSet(); * @property {boolean} [incremental] Whether to interleave data with same-priority streams. * @property {number} [budget] The byte budget for write backpressure. * **Default:** `65536`. + * @property {QuicStream} [webtransportSession] The webtransport session control stream. * @property {boolean} [webtransport] Indicates that the headers should send signal * webtransport support. If no headers are provided, it has no effect. * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers @@ -2703,6 +2706,20 @@ class QuicStream { return sendHeaders(this.#handle, kind, headerString, flags); } + /** + * Attaches a webtransport session to the stream and sends initial bytes + * indicating webtransport stream and the session id + * @returns {boolean} true if it succeeded. + */ + [kMakeWebtransportStream](session) { + if (this.pending) { + debug('pending stream enqueuing makeWebtransportStream for session', session.id); + } else { + debug(`stream ${this.id} makeWebtransportStream for session`, session.id); + } + return makeWebtransportStream(this.#handle, session.#handle); + } + [kFinishClose](error) { const inner = this.#inner; inner.pendingClose ??= PromiseWithResolvers(); @@ -3525,6 +3542,7 @@ class QuicSession { incremental = false, budget = kDefaultBudget, webtransport = false, + webtransportSession = undefined, headers, onheaders, ontrailers, @@ -3542,6 +3560,24 @@ class QuicSession { throw new ERR_INVALID_ARG_TYPE('options.body', 'A body can not be provided in webtransport mode'); } + if (webtransportSession) { + if (webtransport && webtransportSession) { + throw new ERR_INVALID_ARG_TYPE('options.webtransport', + 'webtransport can not be set for ' + + 'creating a webtransport stream associated with a webtransport session'); + } + + if (headers && webtransportSession) { + throw new ERR_INVALID_ARG_TYPE('options.headers', + 'headers can not be set for ' + + 'creating a webtransport stream associated with a webtransport session'); + } + if (!isQuicStream(webtransportSession)) { + throw new ERR_INVALID_ARG_TYPE('options.webtransportSession', + 'webtransportSession was to be of type QuicStream'); + } + } + const handle = this.#handle.openStream(direction, validatedBody); if (handle === undefined) { throw new ERR_QUIC_OPEN_STREAM_FAILED(); @@ -3583,6 +3619,11 @@ class QuicSession { throw new ERR_INVALID_ARG_VALUE('options.webtransport', 'Specifying webtransport without a header has no effect'); } + if (webtransportSession) { + if (!stream[kMakeWebtransportStream](webtransportSession)) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } + } if (onSessionOpenStreamChannel.hasSubscribers) { onSessionOpenStreamChannel.publish({ diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index 1ed44ebe2b13..3f85f2afacc9 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -56,6 +56,7 @@ const kRemoveSession = Symbol('kRemoveSession'); const kRemoveStream = Symbol('kRemoveStream'); const kReset = Symbol('kReset'); const kSendHeaders = Symbol('kSendHeaders'); +const kMakeWebtransportStream = Symbol('kMakeWebtransportStream'); const kSessionApplication = Symbol('kSessionApplication'); const kSessionTicket = Symbol('kSessionTicket'); const kStopSending = Symbol('kStopSending'); @@ -94,6 +95,7 @@ module.exports = { kRemoveStream, kReset, kSendHeaders, + kMakeWebtransportStream, kSessionApplication, kSessionTicket, kStopSending, diff --git a/src/quic/application.h b/src/quic/application.h index 9d207eabd4f1..9b3dde8e8016 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -192,6 +192,13 @@ class Session::Application : public MemoryRetainer { virtual void SetHeadersInterest(Stream& stream, bool wants_headers, bool wants_trailers) {} + // connects the webtransport session stream to stream object, + // it also sends some initial bytes to the wire to signal + // the other side, that this is a webtransport stream + virtual bool MakeWebtransportStream(Stream& stream, + int64_t sessionid) { + return false; + } // Returns true if the application protocol supports sending and // receiving headers on streams (e.g. HTTP/3). Applications that diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index b8460beea07d..c8b283cebf5d 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -294,6 +294,10 @@ void BindingData::InitPerContext(Realm* realm, Local target) { SetMethod(realm->context(), target, "setCallbacks", SetCallbacks); SetMethod(realm->context(), target, "sendHeaders", SendHeaders); SetMethod(realm->context(), target, "setHeadersInterest", SetHeadersInterest); + SetMethod(realm->context(), + target, + "makeWebtransportStream", + MakeWebtransportStream); constexpr int QUIC_STREAM_HEADERS_KIND_HINTS = static_cast(HeadersKind::HINTS); @@ -324,6 +328,7 @@ void BindingData::RegisterExternalReferences( registry->Register(SetCallbacks); registry->Register(SendHeaders); registry->Register(SetHeadersInterest); + registry->Register(MakeWebtransportStream); } JS_METHOD_IMPL(BindingData::SendHeaders) { @@ -350,6 +355,23 @@ JS_METHOD_IMPL(BindingData::SetHeadersInterest) { *stream, args[1]->IsTrue(), args[2]->IsTrue()); } +// Connects a stream to a webtransport session stream, +// also sends the initial bytes of a stream to signel the wt stream +// also connects the readers +JS_METHOD_IMPL(BindingData::MakeWebtransportStream) { + Stream* stream; + CHECK_GT(args.Length(), 1); + ASSIGN_OR_RETURN_UNWRAP(&stream, args[0]); + CHECK(args[1]->IsObject()); + Stream* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[1].As()); + args.GetReturnValue() + .Set(stream->session() + .application().MakeWebtransportStream( + *stream, + session->id())); +} + BindingData::BindingData(Realm* realm, Local object) : BaseObject(realm, object), flush_check_(env(), [this]() { OnFlushCheck(); }) { diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index a5fe6f549cb8..3c6a3bf31307 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -299,6 +299,7 @@ class BindingData final JS_METHOD(SetCallbacks); JS_METHOD(SendHeaders); JS_METHOD(SetHeadersInterest); + JS_METHOD(MakeWebtransportStream); // Lazily-created per-Realm SessionManager. Centralizes CID -> Session // routing so that any endpoint can route packets to any session. diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 4fbc03d58863..337e0a7216d3 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -159,6 +159,7 @@ struct Http3StreamState final : public StreamApplicationState { size_t headers_length = 0; bool wants_headers = false; bool wants_trailers = false; + int64_t pending_webtransport_session_ = -1; }; // Implements the low-level HTTP/3 Application semantics. @@ -533,19 +534,29 @@ class Http3ApplicationImpl final : public Session::Application { 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; + if (state == nullptr) return true; + + if (!state->pending_headers.empty()) { + 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; + } } } + + if (state->pending_webtransport_session_ < 0) return true; + + if (!MakeWebtransportStream(stream, + state->pending_webtransport_session_)) { + return false; + } + state->pending_webtransport_session_ = 0; return true; } @@ -580,6 +591,30 @@ class Http3ApplicationImpl final : public Session::Application { state.wants_trailers = wants_trailers; } + bool MakeWebtransportStream(Stream& stream, int64_t sessionid) override { + if (stream.is_pending()) { + Debug(&session(), + "Enqueing Webtransport Session strean for pending stream"); + auto& state = GetOrCreateStreamState(stream); + state.pending_webtransport_session_ = sessionid; + return true; + } + Session::SendPendingDataScope send_scope(&session()); + static constexpr nghttp3_data_reader reader = {on_read_data_callback}; + const nghttp3_data_reader* reader_ptr = &reader; // can use the same reader + + Debug(&session(), + "Make stream %" PRIu64 " webtransport stream of session %" PRIu64, + stream.id(), + sessionid); + return nghttp3_conn_open_wt_data_stream(*this, + sessionid, + stream.id(), + reader_ptr, + const_cast(&stream)) + == 0; + } + void SetStreamPriority(const Stream& stream, StreamPriority priority, StreamPriorityFlags flags) override { From 4176af41b64b4e4f7aba50be8d714148ecc7dcb5 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Wed, 10 Jun 2026 06:36:59 +0200 Subject: [PATCH 10/37] quic: Implement more webtransport stuff --- lib/internal/quic/diagnostics.js | 2 + lib/internal/quic/quic.js | 73 ++++++++++++++++++++++ lib/internal/quic/state.js | 20 ++++++ lib/internal/quic/symbols.js | 2 + src/quic/application.h | 5 ++ src/quic/bindingdata.cc | 13 ++++ src/quic/bindingdata.h | 2 + src/quic/http3.cc | 103 +++++++++++++++++++++++++++++-- src/quic/streams.cc | 5 +- src/quic/streams.h | 1 + 10 files changed, 218 insertions(+), 8 deletions(-) diff --git a/lib/internal/quic/diagnostics.js b/lib/internal/quic/diagnostics.js index 10dac5693fba..fa6c8aa41a60 100644 --- a/lib/internal/quic/diagnostics.js +++ b/lib/internal/quic/diagnostics.js @@ -33,6 +33,7 @@ const onSessionGoawayChannel = dc.channel('quic.session.goaway'); const onSessionEarlyRejectedChannel = dc.channel('quic.session.early.rejected'); const onStreamClosedChannel = dc.channel('quic.stream.closed'); const onStreamHeadersChannel = dc.channel('quic.stream.headers'); +const onStreamSessionIdChannel = dc.channel('quic.stream.sessioid'); const onStreamTrailersChannel = dc.channel('quic.stream.trailers'); const onStreamInfoChannel = dc.channel('quic.stream.info'); const onStreamResetChannel = dc.channel('quic.stream.reset'); @@ -68,6 +69,7 @@ module.exports = { onSessionEarlyRejectedChannel, onStreamClosedChannel, onStreamHeadersChannel, + onStreamSessionIdChannel, onStreamTrailersChannel, onStreamInfoChannel, onStreamResetChannel, diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 13713d7a0d06..22dcef58828c 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -52,6 +52,7 @@ const { sendHeaders, setCallbacks, setHeadersInterest, + setSessionIdInterest, makeWebtransportStream, // The constants to be exposed to end users for various options. @@ -197,6 +198,7 @@ const { kHandshakeCompleted, kVerifyPeer, kHeaders, + kSessionId, kOwner, kRemoveSession, kKeylog, @@ -272,6 +274,7 @@ const { onSessionEarlyRejectedChannel, onStreamClosedChannel, onStreamHeadersChannel, + onStreamSessionIdChannel, onStreamTrailersChannel, onStreamInfoChannel, onStreamResetChannel, @@ -311,6 +314,7 @@ const endpointRegistry = new SafeSet(); * @property {boolean} [webtransport] Indicates that the headers should send signal * webtransport support. If no headers are provided, it has no effect. * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers + * @property {OnSessionIdCallback} [onsessionid] Callback for incoming sessionid * @property {OnTrailersCallback} [ontrailers] Callback for incoming trailing headers * @property {OnInfoCallback} [oninfo] Callback for informational (1xx) headers * @property {OnWantTrailersCallback} [onwanttrailers] Callback fired when the @@ -477,6 +481,7 @@ const endpointRegistry = new SafeSet(); * @property {OnQlogCallback} [onqlog] qlog data callback. * @property {OnApplicationCallback} [onapplication] application options callback. * @property {OnHeadersCallback} [onheaders] Default per-stream initial-headers callback. + * @property {OnSessionIdCallback} [onsessionid] Default perstream initial callback for incoming sessionid. * @property {OnTrailersCallback} [ontrailers] Default per-stream trailing-headers callback. * @property {OnInfoCallback} [oninfo] Default per-stream informational-headers callback. * @property {OnWantTrailersCallback} [onwanttrailers] Default per-stream @@ -724,6 +729,16 @@ const endpointRegistry = new SafeSet(); * @returns {void} */ +/** + * Called when session id (e.g. for Webtransport streams) is determined + * @callback OnSessionIdCallback + * @this {QuicStream} + * @param {bigint|undefined} sessionid Id of the session stream or undefined + * if no session stream is determined. + * @returns {void} + */ + + /** * Called when trailing headers are received from the peer. * @callback OnTrailersCallback @@ -1028,6 +1043,12 @@ setCallbacks({ this[kOwner][kHeaders](headers, kind); }, + onStreamSessionId(sessionId) { + // Called when the stream C++ handle has determined the sessionId + debug(`stream ${this[kOwner].id} sessionid callback`, sessionId); + this[kOwner][kSessionId](sessionId); + }, + onStreamTrailers() { // Called when the stream C++ handle is ready to receive trailing headers. debug('stream want trailers callback', this[kOwner]); @@ -1335,6 +1356,14 @@ function updateHeaderInterest(handle, inner) { ); } +function updateSessionIdInterest(handle, inner) { + if (handle === undefined) return; + setSessionIdInterest( + handle, + inner.onsessionid !== undefined, + ); +} + /** * Applies session and stream callbacks from an options object to a session. * @param {QuicSession} session @@ -1365,6 +1394,7 @@ function applyCallbacks(session, cbs) { onwanttrailers: cbs.onwanttrailers, }; } + if (cbs.onsessionid) session.onsessionid = cbs.onsessionid; } /** @@ -1629,6 +1659,7 @@ class QuicStream { onreset: undefined, onstopsending: undefined, onheaders: undefined, + onsessionid: undefined, ontrailers: undefined, oninfo: undefined, onwanttrailers: undefined, @@ -1901,6 +1932,24 @@ class QuicStream { updateHeaderInterest(this.#handle, inner); } + /** @type {OnSessionIdCallback} */ + get onsessionid() { + assertIsQuicStream(this); + return this.#inner.onsessionid; + } + + set onsessionid(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onsessionid = undefined; + } else { + validateFunction(fn, 'onsessionid'); + inner.onsessionid = FunctionPrototypeBind(fn, this); + } + updateSessionIdInterest(this.#handle, inner); + } + /** @type {Function|undefined} */ get oninfo() { assertIsQuicStream(this); @@ -2766,6 +2815,7 @@ class QuicStream { inner.onreset = undefined; inner.onstopsending = undefined; inner.onheaders = undefined; + inner.onsessionid = undefined; inner.onerror = undefined; inner.ontrailers = undefined; inner.oninfo = undefined; @@ -2876,6 +2926,22 @@ class QuicStream { } } + [kSessionId](sessionId) { + if (this.destroyed) return; + const inner = this.#inner; + if (onStreamSessionIdChannel.hasSubscribers) { + onStreamSessionIdChannel.publish({ + __proto__: null, + stream: this, + session: inner.session, + sessionId, + }); + } + if (typeof inner.onsessionid === 'function') { + safeCallbackInvoke(inner.onsessionid, this, sessionId); + } + } + [kTrailers]() { if (this.destroyed) return; const inner = this.#inner; @@ -3545,6 +3611,7 @@ class QuicSession { webtransportSession = undefined, headers, onheaders, + onsessionid, ontrailers, oninfo, onwanttrailers, @@ -3607,6 +3674,7 @@ class QuicSession { // Set stream callbacks before sending headers to avoid missing events. if (onheaders) stream.onheaders = onheaders; + if (onsessionid) stream.onsessionid = onsessionid; if (ontrailers) stream.ontrailers = ontrailers; if (oninfo) stream.oninfo = oninfo; if (onwanttrailers) stream.onwanttrailers = onwanttrailers; @@ -4422,6 +4490,7 @@ class QuicSession { const scbs = this[kStreamCallbacks]; if (scbs) { if (scbs.onheaders) stream.onheaders = scbs.onheaders; + if (scbs.onsessionid) stream.onsessionid = scbs.onsessionid; if (scbs.ontrailers) stream.ontrailers = scbs.ontrailers; if (scbs.oninfo) stream.oninfo = scbs.oninfo; if (scbs.onwanttrailers) stream.onwanttrailers = scbs.onwanttrailers; @@ -4807,6 +4876,7 @@ class QuicEndpoint { onapplication, // Stream-level callbacks applied to each incoming stream. onheaders, + onsessionid, ontrailers, oninfo, onwanttrailers, @@ -4836,6 +4906,7 @@ class QuicEndpoint { onqlog, onapplication, onheaders, + onsessionid, ontrailers, oninfo, onwanttrailers, @@ -5619,6 +5690,7 @@ function processSessionOptions(options, config = kEmptyObject) { // Application level options changed, e.g. HTTP/3 settings related // Stream-level callbacks. onheaders, + onsessionid, ontrailers, oninfo, onwanttrailers, @@ -5744,6 +5816,7 @@ function processSessionOptions(options, config = kEmptyObject) { onqlog, onapplication, onheaders, + onsessionid, ontrailers, oninfo, onwanttrailers, diff --git a/lib/internal/quic/state.js b/lib/internal/quic/state.js index 0a47e2e7adc8..dfb2560ddca4 100644 --- a/lib/internal/quic/state.js +++ b/lib/internal/quic/state.js @@ -106,6 +106,7 @@ const { IDX_STATE_STREAM_HAS_OUTBOUND, IDX_STATE_STREAM_HAS_READER, IDX_STATE_STREAM_WANTS_BLOCK, + IDX_STATE_STREAM_WANTS_SESSIONID, IDX_STATE_STREAM_WANTS_RESET, IDX_STATE_STREAM_WANTS_STOP_SENDING, IDX_STATE_STREAM_RECEIVED_EARLY_DATA, @@ -149,6 +150,7 @@ assert(IDX_STATE_STREAM_RESET !== undefined); assert(IDX_STATE_STREAM_HAS_OUTBOUND !== undefined); assert(IDX_STATE_STREAM_HAS_READER !== undefined); assert(IDX_STATE_STREAM_WANTS_BLOCK !== undefined); +assert(IDX_STATE_STREAM_WANTS_SESSIONID !== undefined); assert(IDX_STATE_STREAM_WANTS_RESET !== undefined); assert(IDX_STATE_STREAM_WANTS_STOP_SENDING !== undefined); assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE !== undefined); @@ -826,6 +828,20 @@ class QuicStreamState { DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0); } + /** @type {boolean} */ + get wantsSessionId() { + const handle = this.#handle; + if (handle === undefined) return undefined; + return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_SESSIONID) !== 0; + } + + /** @type {boolean} */ + set wantsSessionId(val) { + const handle = this.#handle; + if (handle === undefined) return; + DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_SESSIONID, val ? 1 : 0); + } + /** @type {boolean} */ get wantsReset() { const handle = this.#handle; @@ -922,6 +938,7 @@ class QuicStreamState { wantsBlock, wantsReset, wantsStopSending, + wantsSessionId, early, resetCode, writeDesiredSize, @@ -941,6 +958,7 @@ class QuicStreamState { wantsBlock, wantsReset, wantsStopSending, + wantsSessionId, early, resetCode: `${resetCode}`, writeDesiredSize, @@ -977,6 +995,7 @@ class QuicStreamState { wantsBlock, wantsReset, wantsStopSending, + wantsSessionId, early, resetCode, writeDesiredSize, @@ -996,6 +1015,7 @@ class QuicStreamState { wantsBlock, wantsReset, wantsStopSending, + wantsSessionId, early, resetCode, writeDesiredSize, diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index 3f85f2afacc9..a946ff421531 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -41,6 +41,7 @@ const kHandshake = Symbol('kHandshake'); const kHandshakeCompleted = Symbol('kHandshakeCompleted'); const kVerifyPeer = Symbol('kVerifyPeer'); const kHeaders = Symbol('kHeaders'); +const kSessionId = Symbol('kSessionId'); const kKeylog = Symbol('kKeylog'); const kListen = Symbol('kListen'); const kQlog = Symbol('kQlog'); @@ -78,6 +79,7 @@ module.exports = { kHandshakeCompleted, kVerifyPeer, kHeaders, + kSessionId, kInspect, kKeylog, kKeyObjectHandle, diff --git a/src/quic/application.h b/src/quic/application.h index 9b3dde8e8016..48c576ee4618 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -192,6 +192,11 @@ class Session::Application : public MemoryRetainer { virtual void SetHeadersInterest(Stream& stream, bool wants_headers, bool wants_trailers) {} + // Updates JavaScript callback interest concerning new webtransport sessions + // Applications without Webtransport support ignore this + virtual void SetSessionIdInterest(Stream& stream, + bool wants_sessionid) {} + // connects the webtransport session stream to stream object, // it also sends some initial bytes to the wire to signal // the other side, that this is a webtransport stream diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index c8b283cebf5d..31fdbfd7fb48 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -294,6 +294,10 @@ void BindingData::InitPerContext(Realm* realm, Local target) { SetMethod(realm->context(), target, "setCallbacks", SetCallbacks); SetMethod(realm->context(), target, "sendHeaders", SendHeaders); SetMethod(realm->context(), target, "setHeadersInterest", SetHeadersInterest); + SetMethod(realm->context(), + target, + "setSessionIdInterest", + SetSessionIdInterest); SetMethod(realm->context(), target, "makeWebtransportStream", @@ -328,6 +332,7 @@ void BindingData::RegisterExternalReferences( registry->Register(SetCallbacks); registry->Register(SendHeaders); registry->Register(SetHeadersInterest); + registry->Register(SetSessionIdInterest); registry->Register(MakeWebtransportStream); } @@ -355,6 +360,14 @@ JS_METHOD_IMPL(BindingData::SetHeadersInterest) { *stream, args[1]->IsTrue(), args[2]->IsTrue()); } +JS_METHOD_IMPL(BindingData::SetSessionIdInterest) { + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args[0]); + CHECK(args[1]->IsBoolean()); + stream->session().application().SetSessionIdInterest( + *stream, args[1]->IsTrue()); +} + // Connects a stream to a webtransport session stream, // also sends the initial bytes of a stream to signel the wt stream // also connects the readers diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 3c6a3bf31307..c1c968f58f13 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -61,6 +61,7 @@ class SessionManager; V(stream_created, StreamCreated) \ V(stream_drain, StreamDrain) \ V(stream_headers, StreamHeaders) \ + V(stream_sessionid, StreamSessionId) \ V(stream_reset, StreamReset) \ V(stream_stop_sending, StreamStopSending) \ V(stream_trailers, StreamTrailers) @@ -300,6 +301,7 @@ class BindingData final JS_METHOD(SendHeaders); JS_METHOD(SetHeadersInterest); JS_METHOD(MakeWebtransportStream); + JS_METHOD(SetSessionIdInterest); // Lazily-created per-Realm SessionManager. Centralizes CID -> Session // routing so that any endpoint can route packets to any session. diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 337e0a7216d3..6999b8b5f69e 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -22,6 +22,7 @@ namespace node { using v8::Array; +using v8::BigInt; using v8::Global; using v8::Integer; using v8::Local; @@ -159,7 +160,12 @@ struct Http3StreamState final : public StreamApplicationState { size_t headers_length = 0; bool wants_headers = false; bool wants_trailers = false; - int64_t pending_webtransport_session_ = -1; + bool wants_session_id = false; + int64_t pending_webtransport_session = -1; + // Until is is clear, that this has a session stream, it is kMaxStreamId + // after this it is -1, if it is not a webtransport stream + // and >= 0 it is a webtransport stream. + stream_id session_id = kMaxStreamId; }; // Implements the low-level HTTP/3 Application semantics. @@ -550,13 +556,13 @@ class Http3ApplicationImpl final : public Session::Application { } } - if (state->pending_webtransport_session_ < 0) return true; + if (state->pending_webtransport_session < 0) return true; if (!MakeWebtransportStream(stream, - state->pending_webtransport_session_)) { + state->pending_webtransport_session)) { return false; } - state->pending_webtransport_session_ = 0; + state->pending_webtransport_session = 0; return true; } @@ -591,12 +597,18 @@ class Http3ApplicationImpl final : public Session::Application { state.wants_trailers = wants_trailers; } + void SetSessionIdInterest(Stream& stream, + bool wants_sessionid) override { + auto& state = GetOrCreateStreamState(stream); + state.wants_session_id = wants_sessionid; + } + bool MakeWebtransportStream(Stream& stream, int64_t sessionid) override { if (stream.is_pending()) { Debug(&session(), "Enqueing Webtransport Session strean for pending stream"); auto& state = GetOrCreateStreamState(stream); - state.pending_webtransport_session_ = sessionid; + state.pending_webtransport_session = sessionid; return true; } Session::SendPendingDataScope send_scope(&session()); @@ -868,6 +880,14 @@ class Http3ApplicationImpl final : public Session::Application { return true; } + void NotifyWTSession(Stream& stream, stream_id session_id) { + auto& state = GetOrCreateStreamState(stream); + if (state.session_id != session_id) { + state.session_id = session_id; + EmitSessionid(stream, session_id); + } + } + void EmitHeaders(Stream& stream) { auto& state = GetOrCreateStreamState(stream); stream.RecordReceivedActivity(); @@ -902,6 +922,18 @@ class Http3ApplicationImpl final : public Session::Application { binding.stream_headers_callback(), arraysize(argv), argv); } + void EmitSessionid(Stream& stream, stream_id session_id) { + auto* state = GetStreamState(stream); + if (!env()->can_call_into_js() || !state->wants_session_id) { + return; + } + CallbackScope cb_scope(&stream); + auto& binding = BindingData::Get(env()); + Local sid = BigInt::New(env()->isolate(), session_id); + stream.MakeCallback( + binding.stream_sessionid_callback(), 1, &sid); + } + void EmitWantTrailers(Stream& stream) { auto* state = GetStreamState(stream); if (!env()->can_call_into_js() || state == nullptr || @@ -1389,6 +1421,63 @@ class Http3ApplicationImpl final : public Session::Application { return NGHTTP3_ERR_CALLBACK_FAILURE; } + static int on_receive_wt_data(nghttp3_conn* conn, + int64_t session_id, + int64_t stream_id, + const uint8_t* data, + size_t datalen, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + + // 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_user_data)) [[likely]] { + BaseObjectPtr 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(stream_id) && + ngtcp2_conn_is_local_stream(session, stream_id)) { + Debug(&session, + "HTTP/3 discarding %zu bytes" + " for destroyed wt local stream %" PRIi64, + datalen, + stream_id); + app.ReturnConnectionCredit(datalen); + return NGTCP2_SUCCESS; + } + + if (auto stream = app.FindOrCreateStream(stream_id)) [[likely]] { + stream->ReceiveData(data, datalen, Stream::ReceiveDataFlags{}); + return NGTCP2_SUCCESS; + } + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + static int on_wt_data_stream_open(nghttp3_conn* conn, + int64_t session_id, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + + if (auto stream = app.FindOrCreateStream(stream_id)) [[likely]] { + app.NotifyWTSession(*stream.get(), session_id); + return NGTCP2_SUCCESS; + } + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + static int on_deferred_consume(nghttp3_conn* conn, stream_id id, size_t consumed, @@ -1591,7 +1680,9 @@ class Http3ApplicationImpl final : public Session::Application { 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}; + nullptr, + on_receive_wt_data, + on_wt_data_stream_open}; }; std::unique_ptr CreateHttp3Application( diff --git a/src/quic/streams.cc b/src/quic/streams.cc index aae72740f871..d9832e94cf35 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -823,7 +823,6 @@ class Stream::Outbound final : public MemoryRetainer { PullUncommitted(std::move(next)); return bob::Status::STATUS_CONTINUE; } - std::move(next)(bob::Status::STATUS_BLOCK, nullptr, 0, [](int) {}); return bob::Status::STATUS_BLOCK; } @@ -1902,7 +1901,9 @@ void Stream::EmitStopSending(const QuicError& error) { void Stream::Schedule(Queue* queue) { // If this stream is not already in the queue to send data, add it. Debug(this, "Scheduled"); - if (outbound_ && stream_queue_.IsEmpty()) queue->PushBack(this); + if (outbound_ && stream_queue_.IsEmpty()) { + queue->PushBack(this); + } } void Stream::Unschedule() { diff --git a/src/quic/streams.h b/src/quic/streams.h index db6caaec088b..0f47fa780654 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -468,6 +468,7 @@ class Stream final : public AsyncWrap, // When a pending stream is finally opened, the NotifyStreamOpened method // will be called and the id will be assigned. void NotifyStreamOpened(stream_id id); + ArenaSlotBase stats_slot_; ArenaSlotBase state_slot_; BaseObjectWeakPtr session_; From 805a95524a03c36deec27722d186cc5a08caa414 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 13 Jun 2026 17:39:43 +0200 Subject: [PATCH 11/37] quic: Webtransport fix incoming streams --- src/quic/http3.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 6999b8b5f69e..f3646033be2b 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -1472,6 +1472,11 @@ class Http3ApplicationImpl final : public Session::Application { NGHTTP3_CALLBACK_SCOPE(app); if (auto stream = app.FindOrCreateStream(stream_id)) [[likely]] { + if (!app.MakeWebtransportStream(*stream.get(), session_id)) { + stream->Destroy(); // close stream forcefully, + // may be use an assert instead? + return NGHTTP3_ERR_CALLBACK_FAILURE; + } app.NotifyWTSession(*stream.get(), session_id); return NGTCP2_SUCCESS; } From 6daaf7d0675f85df0f8d53281787ecbae945827d Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 13 Jun 2026 18:05:45 +0200 Subject: [PATCH 12/37] quic: Do not try to open WT sending on remote unidirectional stream --- src/quic/application.h | 2 ++ src/quic/http3.cc | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/quic/application.h b/src/quic/application.h index 48c576ee4618..cdeba8d6dc05 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -200,6 +200,8 @@ class Session::Application : public MemoryRetainer { // connects the webtransport session stream to stream object, // it also sends some initial bytes to the wire to signal // the other side, that this is a webtransport stream + // it is a noop, if we can not send on this stream incoming + // unidirectional stream virtual bool MakeWebtransportStream(Stream& stream, int64_t sessionid) { return false; diff --git a/src/quic/http3.cc b/src/quic/http3.cc index f3646033be2b..9eb5c345c511 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -619,6 +619,9 @@ class Http3ApplicationImpl final : public Session::Application { "Make stream %" PRIu64 " webtransport stream of session %" PRIu64, stream.id(), sessionid); + // we only need to do this, if we can send data + if (stream.is_remote_unidirectional()) + return true; // so bail out for remote unidirectional streams return nghttp3_conn_open_wt_data_stream(*this, sessionid, stream.id(), From 25433a963ee8706b1e972368fd2900f93bc5dd52 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 20 Jun 2026 11:39:17 +0200 Subject: [PATCH 13/37] quic: add session id callback to test --- test/parallel/test-quic-internal-setcallbacks.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/parallel/test-quic-internal-setcallbacks.mjs b/test/parallel/test-quic-internal-setcallbacks.mjs index 910f1fa97a74..a4d91d51773b 100644 --- a/test/parallel/test-quic-internal-setcallbacks.mjs +++ b/test/parallel/test-quic-internal-setcallbacks.mjs @@ -34,6 +34,7 @@ const callbacks = { onStreamReset() {}, onStreamStopSending() {}, onStreamHeaders() {}, + onStreamSessionId() {}, onStreamTrailers() {}, }; // Fail if any callback is missing From 3d17d52f43575198adf4075b02659bf7a3591ebe Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 20 Jun 2026 17:57:18 +0200 Subject: [PATCH 14/37] quic: WT implement close session callback --- lib/internal/quic/diagnostics.js | 4 +- lib/internal/quic/quic.js | 70 ++++++++++++++++++- lib/internal/quic/state.js | 20 ------ lib/internal/quic/symbols.js | 1 + src/quic/application.h | 5 +- src/quic/bindingdata.cc | 13 ++-- src/quic/bindingdata.h | 3 +- src/quic/http3.cc | 51 +++++++++++++- src/quic/streams.h | 5 +- .../test-quic-internal-setcallbacks.mjs | 1 - 10 files changed, 134 insertions(+), 39 deletions(-) diff --git a/lib/internal/quic/diagnostics.js b/lib/internal/quic/diagnostics.js index fa6c8aa41a60..79448ad714b4 100644 --- a/lib/internal/quic/diagnostics.js +++ b/lib/internal/quic/diagnostics.js @@ -33,7 +33,8 @@ const onSessionGoawayChannel = dc.channel('quic.session.goaway'); const onSessionEarlyRejectedChannel = dc.channel('quic.session.early.rejected'); const onStreamClosedChannel = dc.channel('quic.stream.closed'); const onStreamHeadersChannel = dc.channel('quic.stream.headers'); -const onStreamSessionIdChannel = dc.channel('quic.stream.sessioid'); +const onStreamSessionIdChannel = dc.channel('quic.stream.sessionid'); +const onStreamWTSessionCloseChannel = dc.channel('quic.stream.wtsessionclose') const onStreamTrailersChannel = dc.channel('quic.stream.trailers'); const onStreamInfoChannel = dc.channel('quic.stream.info'); const onStreamResetChannel = dc.channel('quic.stream.reset'); @@ -70,6 +71,7 @@ module.exports = { onStreamClosedChannel, onStreamHeadersChannel, onStreamSessionIdChannel, + onStreamWTSessionCloseChannel, onStreamTrailersChannel, onStreamInfoChannel, onStreamResetChannel, diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 22dcef58828c..b269069d48be 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -199,6 +199,7 @@ const { kVerifyPeer, kHeaders, kSessionId, + kWTSessionClose, kOwner, kRemoveSession, kKeylog, @@ -275,6 +276,7 @@ const { onStreamClosedChannel, onStreamHeadersChannel, onStreamSessionIdChannel, + onStreamWTSessionCloseChannel, onStreamTrailersChannel, onStreamInfoChannel, onStreamResetChannel, @@ -315,6 +317,7 @@ const endpointRegistry = new SafeSet(); * webtransport support. If no headers are provided, it has no effect. * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers * @property {OnSessionIdCallback} [onsessionid] Callback for incoming sessionid + * @property {OnWTSessionCloseCallback} [onwtsessionclose] Callback for incoming close capsules * @property {OnTrailersCallback} [ontrailers] Callback for incoming trailing headers * @property {OnInfoCallback} [oninfo] Callback for informational (1xx) headers * @property {OnWantTrailersCallback} [onwanttrailers] Callback fired when the @@ -482,6 +485,7 @@ const endpointRegistry = new SafeSet(); * @property {OnApplicationCallback} [onapplication] application options callback. * @property {OnHeadersCallback} [onheaders] Default per-stream initial-headers callback. * @property {OnSessionIdCallback} [onsessionid] Default perstream initial callback for incoming sessionid. + * @property {OnWTSessionCloseCallback} [onwtsessionclose] Default perstream initial callback for incoming close capsules * @property {OnTrailersCallback} [ontrailers] Default per-stream trailing-headers callback. * @property {OnInfoCallback} [oninfo] Default per-stream informational-headers callback. * @property {OnWantTrailersCallback} [onwanttrailers] Default per-stream @@ -738,6 +742,15 @@ const endpointRegistry = new SafeSet(); * @returns {void} */ +/** + * Called when session id (e.g. for Webtransport streams) is determined + * @callback OnWTSessionCloseCallback + * @this {QuicStream} + * @param {number} error code from the webtransport session + * @param {string|undefined} error message from the webtransport session + * @returns {void} + */ + /** * Called when trailing headers are received from the peer. @@ -1049,6 +1062,12 @@ setCallbacks({ this[kOwner][kSessionId](sessionId); }, + onStreamWTSessionClose(errorcode, errormessage) { + // Called when the stream C++ handle has received a close capsule + debug(`stream ${this[kOwner].id} wtsessionclose callback`, errorcode, errormessage); + this[kOwner][kWTSessionClose](errorcode, errormessage); + }, + onStreamTrailers() { // Called when the stream C++ handle is ready to receive trailing headers. debug('stream want trailers callback', this[kOwner]); @@ -1356,11 +1375,11 @@ function updateHeaderInterest(handle, inner) { ); } -function updateSessionIdInterest(handle, inner) { +function updateWebtransportInterest(handle, inner) { if (handle === undefined) return; setSessionIdInterest( handle, - inner.onsessionid !== undefined, + inner.onsessionid !== undefined || inner.onwtsessionclose !== undefined, ); } @@ -1395,6 +1414,7 @@ function applyCallbacks(session, cbs) { }; } if (cbs.onsessionid) session.onsessionid = cbs.onsessionid; + if (cbs.onwtsessionclose) session.onwtsessionclose = cbs.onwtsessionclose; } /** @@ -1660,6 +1680,7 @@ class QuicStream { onstopsending: undefined, onheaders: undefined, onsessionid: undefined, + onwtsessionclose: undefined, ontrailers: undefined, oninfo: undefined, onwanttrailers: undefined, @@ -1947,7 +1968,25 @@ class QuicStream { validateFunction(fn, 'onsessionid'); inner.onsessionid = FunctionPrototypeBind(fn, this); } - updateSessionIdInterest(this.#handle, inner); + updateWebtransportInterest(this.#handle, inner); + } + + /** @type {OnWTSessionCloseCallback} */ + get onwtsessionclose() { + assertIsQuicStream(this); + return this.#inner.onwtsessionclose; + } + + set onwtsessionclose(fn) { + assertIsQuicStream(this); + const inner = this.#inner; + if (fn === undefined) { + inner.onwtsessionclose = undefined; + } else { + validateFunction(fn, 'onwtsessionclose'); + inner.onwtsessionclose = FunctionPrototypeBind(fn, this); + } + updateWebtransportInterest(this.#handle, inner); } /** @type {Function|undefined} */ @@ -2816,6 +2855,7 @@ class QuicStream { inner.onstopsending = undefined; inner.onheaders = undefined; inner.onsessionid = undefined; + inner.onwtsessionclose = undefined; inner.onerror = undefined; inner.ontrailers = undefined; inner.oninfo = undefined; @@ -2942,6 +2982,23 @@ class QuicStream { } } + [kWTSessionClose](errorcode, errormessage) { + if (this.destroyed) return; + const inner = this.#inner; + if (onStreamWTSessionCloseChannel.hasSubscribers) { + onStreamWTSessionCloseChannel.publish({ + __proto__: null, + stream: this, + session: inner.session, + errorcode, + errormessage + }); + } + if (typeof inner.onwtsessionclose === 'function') { + safeCallbackInvoke(inner.onwtsessionclose, this, errorcode, errormessage); + } + } + [kTrailers]() { if (this.destroyed) return; const inner = this.#inner; @@ -3612,6 +3669,7 @@ class QuicSession { headers, onheaders, onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -3675,6 +3733,7 @@ class QuicSession { // Set stream callbacks before sending headers to avoid missing events. if (onheaders) stream.onheaders = onheaders; if (onsessionid) stream.onsessionid = onsessionid; + if (onwtsessionclose) stream.onwtsessionclose = onwtsessionclose; if (ontrailers) stream.ontrailers = ontrailers; if (oninfo) stream.oninfo = oninfo; if (onwanttrailers) stream.onwanttrailers = onwanttrailers; @@ -4491,6 +4550,7 @@ class QuicSession { if (scbs) { if (scbs.onheaders) stream.onheaders = scbs.onheaders; if (scbs.onsessionid) stream.onsessionid = scbs.onsessionid; + if (scbs.onwtsessionclose) stream.onwtsessionclose = scbs.onwtsessionclose; if (scbs.ontrailers) stream.ontrailers = scbs.ontrailers; if (scbs.oninfo) stream.oninfo = scbs.oninfo; if (scbs.onwanttrailers) stream.onwanttrailers = scbs.onwanttrailers; @@ -4877,6 +4937,7 @@ class QuicEndpoint { // Stream-level callbacks applied to each incoming stream. onheaders, onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -4907,6 +4968,7 @@ class QuicEndpoint { onapplication, onheaders, onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -5691,6 +5753,7 @@ function processSessionOptions(options, config = kEmptyObject) { // Stream-level callbacks. onheaders, onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, @@ -5817,6 +5880,7 @@ function processSessionOptions(options, config = kEmptyObject) { onapplication, onheaders, onsessionid, + onwtsessionclose, ontrailers, oninfo, onwanttrailers, diff --git a/lib/internal/quic/state.js b/lib/internal/quic/state.js index dfb2560ddca4..0a47e2e7adc8 100644 --- a/lib/internal/quic/state.js +++ b/lib/internal/quic/state.js @@ -106,7 +106,6 @@ const { IDX_STATE_STREAM_HAS_OUTBOUND, IDX_STATE_STREAM_HAS_READER, IDX_STATE_STREAM_WANTS_BLOCK, - IDX_STATE_STREAM_WANTS_SESSIONID, IDX_STATE_STREAM_WANTS_RESET, IDX_STATE_STREAM_WANTS_STOP_SENDING, IDX_STATE_STREAM_RECEIVED_EARLY_DATA, @@ -150,7 +149,6 @@ assert(IDX_STATE_STREAM_RESET !== undefined); assert(IDX_STATE_STREAM_HAS_OUTBOUND !== undefined); assert(IDX_STATE_STREAM_HAS_READER !== undefined); assert(IDX_STATE_STREAM_WANTS_BLOCK !== undefined); -assert(IDX_STATE_STREAM_WANTS_SESSIONID !== undefined); assert(IDX_STATE_STREAM_WANTS_RESET !== undefined); assert(IDX_STATE_STREAM_WANTS_STOP_SENDING !== undefined); assert(IDX_STATE_STREAM_WRITE_DESIRED_SIZE !== undefined); @@ -828,20 +826,6 @@ class QuicStreamState { DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0); } - /** @type {boolean} */ - get wantsSessionId() { - const handle = this.#handle; - if (handle === undefined) return undefined; - return DataViewPrototypeGetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_SESSIONID) !== 0; - } - - /** @type {boolean} */ - set wantsSessionId(val) { - const handle = this.#handle; - if (handle === undefined) return; - DataViewPrototypeSetUint8(handle, this.#offset + IDX_STATE_STREAM_WANTS_SESSIONID, val ? 1 : 0); - } - /** @type {boolean} */ get wantsReset() { const handle = this.#handle; @@ -938,7 +922,6 @@ class QuicStreamState { wantsBlock, wantsReset, wantsStopSending, - wantsSessionId, early, resetCode, writeDesiredSize, @@ -958,7 +941,6 @@ class QuicStreamState { wantsBlock, wantsReset, wantsStopSending, - wantsSessionId, early, resetCode: `${resetCode}`, writeDesiredSize, @@ -995,7 +977,6 @@ class QuicStreamState { wantsBlock, wantsReset, wantsStopSending, - wantsSessionId, early, resetCode, writeDesiredSize, @@ -1015,7 +996,6 @@ class QuicStreamState { wantsBlock, wantsReset, wantsStopSending, - wantsSessionId, early, resetCode, writeDesiredSize, diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index a946ff421531..b9be40e011b0 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -42,6 +42,7 @@ const kHandshakeCompleted = Symbol('kHandshakeCompleted'); const kVerifyPeer = Symbol('kVerifyPeer'); const kHeaders = Symbol('kHeaders'); const kSessionId = Symbol('kSessionId'); +const kWTSessionClose = Symbol('kWTSessionClose'); const kKeylog = Symbol('kKeylog'); const kListen = Symbol('kListen'); const kQlog = Symbol('kQlog'); diff --git a/src/quic/application.h b/src/quic/application.h index cdeba8d6dc05..d89f9752f5d9 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -194,8 +194,9 @@ class Session::Application : public MemoryRetainer { bool wants_trailers) {} // Updates JavaScript callback interest concerning new webtransport sessions // Applications without Webtransport support ignore this - virtual void SetSessionIdInterest(Stream& stream, - bool wants_sessionid) {} + virtual void SetWebtransportInterest(Stream& stream, + bool wants_sessionid, + bool wants_wtsessionclose) {} // connects the webtransport session stream to stream object, // it also sends some initial bytes to the wire to signal diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index 31fdbfd7fb48..e338951d642c 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -296,8 +296,8 @@ void BindingData::InitPerContext(Realm* realm, Local target) { SetMethod(realm->context(), target, "setHeadersInterest", SetHeadersInterest); SetMethod(realm->context(), target, - "setSessionIdInterest", - SetSessionIdInterest); + "setWebtransportInterest", + SetWebtransportInterest); SetMethod(realm->context(), target, "makeWebtransportStream", @@ -332,7 +332,7 @@ void BindingData::RegisterExternalReferences( registry->Register(SetCallbacks); registry->Register(SendHeaders); registry->Register(SetHeadersInterest); - registry->Register(SetSessionIdInterest); + registry->Register(SetWebtransportInterest); registry->Register(MakeWebtransportStream); } @@ -360,12 +360,13 @@ JS_METHOD_IMPL(BindingData::SetHeadersInterest) { *stream, args[1]->IsTrue(), args[2]->IsTrue()); } -JS_METHOD_IMPL(BindingData::SetSessionIdInterest) { +JS_METHOD_IMPL(BindingData::SetWebtransportInterest) { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args[0]); CHECK(args[1]->IsBoolean()); - stream->session().application().SetSessionIdInterest( - *stream, args[1]->IsTrue()); + CHECK(args[2]->IsBoolean()); + stream->session().application().SetWebtransportInterest( + *stream, args[1]->IsTrue(), args[2]->IsTrue()); } // Connects a stream to a webtransport session stream, diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index c1c968f58f13..542ab1cd4e5d 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -62,6 +62,7 @@ class SessionManager; V(stream_drain, StreamDrain) \ V(stream_headers, StreamHeaders) \ V(stream_sessionid, StreamSessionId) \ + V(stream_wtsessionclose, StreamWTSessionClose) \ V(stream_reset, StreamReset) \ V(stream_stop_sending, StreamStopSending) \ V(stream_trailers, StreamTrailers) @@ -301,7 +302,7 @@ class BindingData final JS_METHOD(SendHeaders); JS_METHOD(SetHeadersInterest); JS_METHOD(MakeWebtransportStream); - JS_METHOD(SetSessionIdInterest); + JS_METHOD(SetWebtransportInterest); // Lazily-created per-Realm SessionManager. Centralizes CID -> Session // routing so that any endpoint can route packets to any session. diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 9eb5c345c511..525177d03af7 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -27,6 +27,7 @@ using v8::Global; using v8::Integer; using v8::Local; using v8::LocalVector; +using v8::String; using v8::Value; namespace quic { @@ -161,6 +162,7 @@ struct Http3StreamState final : public StreamApplicationState { bool wants_headers = false; bool wants_trailers = false; bool wants_session_id = false; + bool wants_wtsessionclose = false; int64_t pending_webtransport_session = -1; // Until is is clear, that this has a session stream, it is kMaxStreamId // after this it is -1, if it is not a webtransport stream @@ -597,10 +599,12 @@ class Http3ApplicationImpl final : public Session::Application { state.wants_trailers = wants_trailers; } - void SetSessionIdInterest(Stream& stream, - bool wants_sessionid) override { + void SetWebtransportInterest(Stream& stream, + bool wants_sessionid, + bool wants_wtsessionclose) override { auto& state = GetOrCreateStreamState(stream); state.wants_session_id = wants_sessionid; + state.wants_wtsessionclose = wants_wtsessionclose; } bool MakeWebtransportStream(Stream& stream, int64_t sessionid) override { @@ -891,6 +895,13 @@ class Http3ApplicationImpl final : public Session::Application { } } + void NotifyWTSessionClose(Stream& stream, + uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen) { + EmitWTSessionClose(stream, wt_error_code, msg, msglen); + } + void EmitHeaders(Stream& stream) { auto& state = GetOrCreateStreamState(stream); stream.RecordReceivedActivity(); @@ -937,6 +948,24 @@ class Http3ApplicationImpl final : public Session::Application { binding.stream_sessionid_callback(), 1, &sid); } + void EmitWTSessionClose(Stream& stream, + uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen) { + auto* state = GetStreamState(stream); + if (!env()->can_call_into_js() || !state->wants_wtsessionclose) return; + CallbackScope cb_scope(&stream); + auto& binding = BindingData::Get(env()); + Local argv[] = { + Integer::NewFromUnsigned(env()->isolate(), + wt_error_code), + String::NewFromUtf8(env()->isolate(), reinterpret_cast(msg), + v8::NewStringType::kNormal, msglen).ToLocalChecked() + }; + stream.MakeCallback(binding.stream_wtsessionclose_callback(), + arraysize(argv), argv); +} + void EmitWantTrailers(Stream& stream) { auto* state = GetStreamState(stream); if (!env()->can_call_into_js() || state == nullptr || @@ -1486,6 +1515,21 @@ class Http3ApplicationImpl final : public Session::Application { return NGHTTP3_ERR_CALLBACK_FAILURE; } + static int on_recv_wt_close_session(nghttp3_conn *conn, + int64_t session_id, + uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen, + void *conn_user_data, + void *stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + if (auto stream = app.FindOrCreateStream(session_id)) [[likely]] { + app.NotifyWTSessionClose(*stream.get(), wt_error_code, msg, msglen); + return NGTCP2_SUCCESS; + } + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + static int on_deferred_consume(nghttp3_conn* conn, stream_id id, size_t consumed, @@ -1690,7 +1734,8 @@ class Http3ApplicationImpl final : public Session::Application { // ReceiveStreamClose requests it, when we've already handled this. nullptr, on_receive_wt_data, - on_wt_data_stream_open}; + on_wt_data_stream_open, + on_recv_wt_close_session}; }; std::unique_ptr CreateHttp3Application( diff --git a/src/quic/streams.h b/src/quic/streams.h index 0f47fa780654..e43f7506ac68 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -295,6 +295,9 @@ class Stream final : public AsyncWrap, // True if a Blob::Reader has been created for the inbound data. bool has_reader() const; + bool is_local_unidirectional() const; + bool is_remote_unidirectional() const; + // Returns the Blob::Reader for the inbound data, or nullptr. Blob::Reader* reader() const; @@ -435,8 +438,6 @@ class Stream final : public AsyncWrap, void WriteStreamData(const v8::FunctionCallbackInfo& args); void EndWriting(); - bool is_local_unidirectional() const; - bool is_remote_unidirectional() const; void ReleaseArenaSlots(); // JavaScript callouts diff --git a/test/parallel/test-quic-internal-setcallbacks.mjs b/test/parallel/test-quic-internal-setcallbacks.mjs index a4d91d51773b..910f1fa97a74 100644 --- a/test/parallel/test-quic-internal-setcallbacks.mjs +++ b/test/parallel/test-quic-internal-setcallbacks.mjs @@ -34,7 +34,6 @@ const callbacks = { onStreamReset() {}, onStreamStopSending() {}, onStreamHeaders() {}, - onStreamSessionId() {}, onStreamTrailers() {}, }; // Fail if any callback is missing From ce4adee0c5f06458e1ae12184db89c7d9ad9f517 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 27 Jun 2026 10:45:59 +0200 Subject: [PATCH 15/37] quic: Implement webtransport close session stream --- lib/internal/quic/quic.js | 47 ++++++++++++++++++++++++++++++++++++ lib/internal/quic/symbols.js | 2 ++ src/quic/application.h | 11 +++++++++ src/quic/bindingdata.cc | 33 +++++++++++++++++++++++++ src/quic/bindingdata.h | 1 + src/quic/http3.cc | 21 ++++++++++++++++ 6 files changed, 115 insertions(+) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index b269069d48be..46c3d2ced620 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -54,6 +54,7 @@ const { setHeadersInterest, setSessionIdInterest, makeWebtransportStream, + closeWebtransportSessionStream, // The constants to be exposed to end users for various options. CC_ALGO_RENO_STR: CC_ALGO_RENO, @@ -216,6 +217,7 @@ const { kReset, kSendHeaders, kMakeWebtransportStream, + kCloseWebtransportSessionStream, kSessionApplication, kSessionTicket, kStopSending, @@ -2142,6 +2144,33 @@ class QuicStream { return this.#inner.pendingClose.promise; } + /** + * Only for webtransport session streams + * Closes the webtransport session stream, and also closes + * connected datastream internally. + * @param {number|undefined} code optional error code + * @param {string|undefined} msg optional error message truncated to 1024 bytes + */ + closeWebtransportSessionStream(code, msg) { + assertIsQuicStream(this); + const inner = this.#inner; + if (inner.destroying || this.destroyed) return; + + if (msg !== undefined) { + validateString(msg, 'msg'); + } + inner.destroying = true; + const handle = this.#handle; + const error = makeQuicError( + 'ERR_QUIC_APPLICATION_ERROR', + 'Webtransport error', + 'application', + code ?? 0, + msg ?? ''); + this[kFinishClose](error); + handle.destroy(); + } + /** * Immediately destroys the stream. Any queued data is discarded. If * an error is given, the closed promise will be rejected with that @@ -2797,6 +2826,7 @@ class QuicStream { /** * Attaches a webtransport session to the stream and sends initial bytes * indicating webtransport stream and the session id + * @param {QuicStream} session Webtransport session stream * @returns {boolean} true if it succeeded. */ [kMakeWebtransportStream](session) { @@ -2808,6 +2838,23 @@ class QuicStream { return makeWebtransportStream(this.#handle, session.#handle); } + /** + * Closes a webtransport session stream and also closes associated data streams + * Passing optional error code and error message (limited to 1024 bytes). + * @param {number} code error code + * @param {string} msg error message limited to 1024 bytes + * @returns {boolean} true if it succeeded. + */ + [kCloseWebtransportSessionStream](code, msg) { + if (this.pending) { + debug('pending stream enqueuing closeWebtransportSessionStream with code', code, ' and msg:', msg); + } else { + debug(`stream ${this.id} closeWebtransportSessionStream with code`, code, + ' and msg:', msg); + } + return closeWebtransportSessionStream(this.#handle, session.#handle, code, msg); + } + [kFinishClose](error) { const inner = this.#inner; inner.pendingClose ??= PromiseWithResolvers(); diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index b9be40e011b0..b641b4f14bd7 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -59,6 +59,7 @@ const kRemoveStream = Symbol('kRemoveStream'); const kReset = Symbol('kReset'); const kSendHeaders = Symbol('kSendHeaders'); const kMakeWebtransportStream = Symbol('kMakeWebtransportStream'); +const kCloseWebtransportSessionStream = Symbol('kCloseWebtransportSessionStream'); const kSessionApplication = Symbol('kSessionApplication'); const kSessionTicket = Symbol('kSessionTicket'); const kStopSending = Symbol('kStopSending'); @@ -99,6 +100,7 @@ module.exports = { kReset, kSendHeaders, kMakeWebtransportStream, + kCloseWebtransportSessionStream, kSessionApplication, kSessionTicket, kStopSending, diff --git a/src/quic/application.h b/src/quic/application.h index d89f9752f5d9..f9ffd40c2f24 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -208,6 +208,17 @@ class Session::Application : public MemoryRetainer { return false; } + // closes the webtransort session stream, + // and also closes connect webtransport data streams + virtual bool CloseWebtransportSessionStream( + const Stream& stream, + uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen + ) { + return false; + } + // Returns true if the application protocol supports sending and // receiving headers on streams (e.g. HTTP/3). Applications that // do not support headers should return false (the default). diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index e338951d642c..f8a76a7af596 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -386,6 +386,39 @@ JS_METHOD_IMPL(BindingData::MakeWebtransportStream) { session->id())); } +// Closes a webtransport session stream, +// also closes connected data streams +JS_METHOD_IMPL(BindingData::CloseWebtransportSessionStream) { + Stream* stream; + CHECK_GT(args.Length(), 1); + ASSIGN_OR_RETURN_UNWRAP(&stream, args[0]); + uint32_t wt_error_code = 0; + if (args.Length() > 1) { + CHECK(args[1]->IsUint32()); + wt_error_code = FromV8Value(args[1]); + } + uint8_t * msg = nullptr; + size_t msglen = 0; + if (args.Length() > 2) { + CHECK(args[2]->IsString()); + Local msgstr = args[2].As(); + const size_t length = msgstr->Utf8LengthV2(args.GetIsolate()); + msg = new uint8_t[length]; + msgstr->WriteUtf8V2( + args.GetIsolate(), reinterpret_cast(msg), length, String::WriteFlags::kNone); + msglen = std::min(length, 1024); + } + args.GetReturnValue().Set(stream->session().application().CloseWebtransportSessionStream( + *stream, + wt_error_code, + msg, + msglen + )); + if (msg) { + delete[] msg; + } +} + BindingData::BindingData(Realm* realm, Local object) : BaseObject(realm, object), flush_check_(env(), [this]() { OnFlushCheck(); }) { diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 542ab1cd4e5d..37a78ef11cc0 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -303,6 +303,7 @@ class BindingData final JS_METHOD(SetHeadersInterest); JS_METHOD(MakeWebtransportStream); JS_METHOD(SetWebtransportInterest); + JS_METHOD(CloseWebtransportSessionStream); // Lazily-created per-Realm SessionManager. Centralizes CID -> Session // routing so that any endpoint can route packets to any session. diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 525177d03af7..f3940bd0d7db 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -634,6 +634,27 @@ class Http3ApplicationImpl final : public Session::Application { == 0; } + // closes the webtransort session stream, + // and also closes connect webtransport data streams + // msg is optional + // msg length is maximum 1024 + bool CloseWebtransportSessionStream( + const Stream& stream, + uint32_t wt_error_code, + const uint8_t *msg, + size_t msglen + ) override { + Session::SendPendingDataScope send_scope(&session()); + Debug(&session(), + "Close webtransport session stream %" PRIu64, + stream.id()); + return nghttp3_conn_close_wt_session(*this, + stream.id(), + wt_error_code, + msg, + msglen) == 0; + } + void SetStreamPriority(const Stream& stream, StreamPriority priority, StreamPriorityFlags flags) override { From 73b09599d0c8ec046e35e3ffc825d06513fa4888 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 5 Jul 2026 09:34:03 +0200 Subject: [PATCH 16/37] quic: fixes in close webtransport session --- lib/internal/quic/quic.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 46c3d2ced620..27f9e5418612 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -2159,8 +2159,12 @@ class QuicStream { if (msg !== undefined) { validateString(msg, 'msg'); } + if (code !== undefined) { + validateInteger(code); + } inner.destroying = true; const handle = this.#handle; + this[kCloseWebtransportSessionStream](code, msg); const error = makeQuicError( 'ERR_QUIC_APPLICATION_ERROR', 'Webtransport error', From b780a012179a1498d8c405900ee333ef43039f02 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Tue, 7 Jul 2026 07:58:40 +0200 Subject: [PATCH 17/37] quic: Add to close capsule --- lib/internal/quic/diagnostics.js | 2 +- lib/internal/quic/quic.js | 23 ++++++++++++----------- lib/internal/quic/symbols.js | 13 +++++++------ src/quic/application.h | 2 +- src/quic/bindingdata.cc | 10 ++++++---- src/quic/http3.cc | 26 +++++++++++++------------- 6 files changed, 40 insertions(+), 36 deletions(-) diff --git a/lib/internal/quic/diagnostics.js b/lib/internal/quic/diagnostics.js index 79448ad714b4..afa2bf4fec7f 100644 --- a/lib/internal/quic/diagnostics.js +++ b/lib/internal/quic/diagnostics.js @@ -34,7 +34,7 @@ const onSessionEarlyRejectedChannel = dc.channel('quic.session.early.rejected'); const onStreamClosedChannel = dc.channel('quic.stream.closed'); const onStreamHeadersChannel = dc.channel('quic.stream.headers'); const onStreamSessionIdChannel = dc.channel('quic.stream.sessionid'); -const onStreamWTSessionCloseChannel = dc.channel('quic.stream.wtsessionclose') +const onStreamWTSessionCloseChannel = dc.channel('quic.stream.wtsessionclose'); const onStreamTrailersChannel = dc.channel('quic.stream.trailers'); const onStreamInfoChannel = dc.channel('quic.stream.info'); const onStreamResetChannel = dc.channel('quic.stream.reset'); diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 27f9e5418612..e919395c98db 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -487,7 +487,8 @@ const endpointRegistry = new SafeSet(); * @property {OnApplicationCallback} [onapplication] application options callback. * @property {OnHeadersCallback} [onheaders] Default per-stream initial-headers callback. * @property {OnSessionIdCallback} [onsessionid] Default perstream initial callback for incoming sessionid. - * @property {OnWTSessionCloseCallback} [onwtsessionclose] Default perstream initial callback for incoming close capsules + * @property {OnWTSessionCloseCallback} [onwtsessionclose] Default perstream + * initial callback for incoming close capsules * @property {OnTrailersCallback} [ontrailers] Default per-stream trailing-headers callback. * @property {OnInfoCallback} [oninfo] Default per-stream informational-headers callback. * @property {OnWantTrailersCallback} [onwanttrailers] Default per-stream @@ -749,7 +750,7 @@ const endpointRegistry = new SafeSet(); * @callback OnWTSessionCloseCallback * @this {QuicStream} * @param {number} error code from the webtransport session - * @param {string|undefined} error message from the webtransport session + * @param {string|undefined} error message from the webtransport session * @returns {void} */ @@ -2166,11 +2167,11 @@ class QuicStream { const handle = this.#handle; this[kCloseWebtransportSessionStream](code, msg); const error = makeQuicError( - 'ERR_QUIC_APPLICATION_ERROR', - 'Webtransport error', - 'application', - code ?? 0, - msg ?? ''); + 'ERR_QUIC_APPLICATION_ERROR', + 'Webtransport error', + 'application', + code ?? 0, + msg ?? ''); this[kFinishClose](error); handle.destroy(); } @@ -2842,7 +2843,7 @@ class QuicStream { return makeWebtransportStream(this.#handle, session.#handle); } - /** + /** * Closes a webtransport session stream and also closes associated data streams * Passing optional error code and error message (limited to 1024 bytes). * @param {number} code error code @@ -2854,9 +2855,9 @@ class QuicStream { debug('pending stream enqueuing closeWebtransportSessionStream with code', code, ' and msg:', msg); } else { debug(`stream ${this.id} closeWebtransportSessionStream with code`, code, - ' and msg:', msg); + ' and msg:', msg); } - return closeWebtransportSessionStream(this.#handle, session.#handle, code, msg); + return closeWebtransportSessionStream(this.#handle, code, msg); } [kFinishClose](error) { @@ -3042,7 +3043,7 @@ class QuicStream { stream: this, session: inner.session, errorcode, - errormessage + errormessage, }); } if (typeof inner.onwtsessionclose === 'function') { diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index b641b4f14bd7..e660a2ba0164 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -30,6 +30,7 @@ const { const kAttachFileHandle = Symbol('kAttachFileHandle'); const kAvailable = Symbol('kAvailable'); const kBlocked = Symbol('kBlocked'); +const kCloseWebtransportSessionStream = Symbol('kCloseWebtransportSessionStream'); const kConnect = Symbol('kConnect'); const kDrain = Symbol('kDrain'); const kDatagram = Symbol('kDatagram'); @@ -41,11 +42,10 @@ const kHandshake = Symbol('kHandshake'); const kHandshakeCompleted = Symbol('kHandshakeCompleted'); const kVerifyPeer = Symbol('kVerifyPeer'); const kHeaders = Symbol('kHeaders'); -const kSessionId = Symbol('kSessionId'); -const kWTSessionClose = Symbol('kWTSessionClose'); const kKeylog = Symbol('kKeylog'); const kListen = Symbol('kListen'); const kQlog = Symbol('kQlog'); +const kMakeWebtransportStream = Symbol('kMakeWebtransportStream'); const kNewSession = Symbol('kNewSession'); const kNewStream = Symbol('kNewStream'); const kNewToken = Symbol('kNewToken'); @@ -58,13 +58,13 @@ const kRemoveSession = Symbol('kRemoveSession'); const kRemoveStream = Symbol('kRemoveStream'); const kReset = Symbol('kReset'); const kSendHeaders = Symbol('kSendHeaders'); -const kMakeWebtransportStream = Symbol('kMakeWebtransportStream'); -const kCloseWebtransportSessionStream = Symbol('kCloseWebtransportSessionStream'); const kSessionApplication = Symbol('kSessionApplication'); +const kSessionId = Symbol('kSessionId'); const kSessionTicket = Symbol('kSessionTicket'); const kStopSending = Symbol('kStopSending'); const kTrailers = Symbol('kTrailers'); const kVersionNegotiation = Symbol('kVersionNegotiation'); +const kWTSessionClose = Symbol('kWTSessionClose'); module.exports = { kAttachFileHandle, @@ -81,11 +81,11 @@ module.exports = { kHandshakeCompleted, kVerifyPeer, kHeaders, - kSessionId, kInspect, kKeylog, kKeyObjectHandle, kListen, + kMakeWebtransportStream, kNewSession, kNewStream, kNewToken, @@ -99,13 +99,14 @@ module.exports = { kRemoveStream, kReset, kSendHeaders, - kMakeWebtransportStream, kCloseWebtransportSessionStream, kSessionApplication, + kSessionId, kSessionTicket, kStopSending, kTrailers, kVersionNegotiation, + kWTSessionClose, }; /* c8 ignore stop */ diff --git a/src/quic/application.h b/src/quic/application.h index f9ffd40c2f24..ace39e9e6749 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -213,7 +213,7 @@ class Session::Application : public MemoryRetainer { virtual bool CloseWebtransportSessionStream( const Stream& stream, uint32_t wt_error_code, - const uint8_t *msg, + const uint8_t* msg, size_t msglen ) { return false; diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index f8a76a7af596..4b03a11fffc1 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -405,15 +405,17 @@ JS_METHOD_IMPL(BindingData::CloseWebtransportSessionStream) { const size_t length = msgstr->Utf8LengthV2(args.GetIsolate()); msg = new uint8_t[length]; msgstr->WriteUtf8V2( - args.GetIsolate(), reinterpret_cast(msg), length, String::WriteFlags::kNone); + args.GetIsolate(), reinterpret_cast(msg), + length, + String::WriteFlags::kNone); msglen = std::min(length, 1024); } - args.GetReturnValue().Set(stream->session().application().CloseWebtransportSessionStream( + args.GetReturnValue().Set(stream->session().application() + .CloseWebtransportSessionStream( *stream, wt_error_code, msg, - msglen - )); + msglen)); if (msg) { delete[] msg; } diff --git a/src/quic/http3.cc b/src/quic/http3.cc index f3940bd0d7db..886ceea482e7 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -625,7 +625,7 @@ class Http3ApplicationImpl final : public Session::Application { sessionid); // we only need to do this, if we can send data if (stream.is_remote_unidirectional()) - return true; // so bail out for remote unidirectional streams + return true; // so bail out for remote unidirectional streams return nghttp3_conn_open_wt_data_stream(*this, sessionid, stream.id(), @@ -641,9 +641,8 @@ class Http3ApplicationImpl final : public Session::Application { bool CloseWebtransportSessionStream( const Stream& stream, uint32_t wt_error_code, - const uint8_t *msg, - size_t msglen - ) override { + const uint8_t* msg, + size_t msglen) override { Session::SendPendingDataScope send_scope(&session()); Debug(&session(), "Close webtransport session stream %" PRIu64, @@ -918,7 +917,7 @@ class Http3ApplicationImpl final : public Session::Application { void NotifyWTSessionClose(Stream& stream, uint32_t wt_error_code, - const uint8_t *msg, + const uint8_t* msg, size_t msglen) { EmitWTSessionClose(stream, wt_error_code, msg, msglen); } @@ -971,19 +970,20 @@ class Http3ApplicationImpl final : public Session::Application { void EmitWTSessionClose(Stream& stream, uint32_t wt_error_code, - const uint8_t *msg, + const uint8_t* msg, size_t msglen) { - auto* state = GetStreamState(stream); + auto* state = GetStreamState(stream); if (!env()->can_call_into_js() || !state->wants_wtsessionclose) return; CallbackScope cb_scope(&stream); auto& binding = BindingData::Get(env()); Local argv[] = { Integer::NewFromUnsigned(env()->isolate(), wt_error_code), - String::NewFromUtf8(env()->isolate(), reinterpret_cast(msg), + String::NewFromUtf8(env()->isolate(), + reinterpret_cast(msg), v8::NewStringType::kNormal, msglen).ToLocalChecked() }; - stream.MakeCallback(binding.stream_wtsessionclose_callback(), + stream.MakeCallback(binding.stream_wtsessionclose_callback(), arraysize(argv), argv); } @@ -1536,13 +1536,13 @@ class Http3ApplicationImpl final : public Session::Application { return NGHTTP3_ERR_CALLBACK_FAILURE; } - static int on_recv_wt_close_session(nghttp3_conn *conn, + static int on_recv_wt_close_session(nghttp3_conn* conn, int64_t session_id, uint32_t wt_error_code, - const uint8_t *msg, + const uint8_t* msg, size_t msglen, - void *conn_user_data, - void *stream_user_data) { + void* conn_user_data, + void* stream_user_data) { NGHTTP3_CALLBACK_SCOPE(app); if (auto stream = app.FindOrCreateStream(session_id)) [[likely]] { app.NotifyWTSessionClose(*stream.get(), wt_error_code, msg, msglen); From 7a6119e5118433fd63ae4331b41a2920c52db225 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 2 Aug 2026 08:51:24 +0200 Subject: [PATCH 18/37] quic: test for establishing a wt session --- test/parallel/test-quic-h3-wt-session.mjs | 125 ++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 test/parallel/test-quic-h3-wt-session.mjs diff --git a/test/parallel/test-quic-h3-wt-session.mjs b/test/parallel/test-quic-h3-wt-session.mjs new file mode 100644 index 000000000000..018efe7277c2 --- /dev/null +++ b/test/parallel/test-quic-h3-wt-session.mjs @@ -0,0 +1,125 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 webtransport session establishment +// Verifies that a session established and properly closed + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { setTimeout: sleep } = await import('timers/promises'); + +const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); +const cert = fixtures.readKey('agent1-cert.pem'); + +const serverSessionOpened = Promise.withResolvers(); + + +const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onapplication = mustCall((aopts) => { + assert.strictEqual(!aopts.enableDatagrams, false); + }); + ss.onhandshake = mustCall(function() { + assert.strictEqual(BigInt(this?.remoteTransportParams?.maxDatagramFrameSize) > 0, true); + }); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + application: { + enableConnectProtocol: true, + enableDatagrams: true, + enableWebtransport: true + }, + transportParams: { maxDatagramFrameSize: 100 }, + onheaders: mustCall(function(headers) { + try { + assert.strictEqual(headers[':scheme'], 'https'); + assert.strictEqual(headers[':method'], 'CONNECT'); + assert.strictEqual(headers[':protocol'], 'webtransport'); // depends on the draft + assert.strictEqual(headers[':path'], '/testwtpath'); + // We could also check for wt-available protocols + this.sendHeaders( + { ':status': '200' }, + { terminal: false, webtransport: true } + ); + serverSessionOpened.resolve(); + } catch (error) { + serverSessionOpened.reject(error); + } + // Should only be installed on wt streams + this.onwtsessionclose = mustCall((code, reason) => { + assert.strictEqual(code, 200); + assert.strictEqual(reason, 'all perfect'); + this.session.close(); + }); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + application: { + enableConnectProtocol: true, + enableDatagrams: true, + enableWebtransport: true + }, + transportParams: { maxDatagramFrameSize: 1000 }, +}); + +const webtransportSupport = Promise.withResolvers(); + +clientSession.onapplication = mustCall((aopts) => { + try { + // Test for webtransport support + assert.strictEqual(aopts.enableConnectProtocol, true); + assert.strictEqual(aopts.enableDatagrams, true); + assert.strictEqual(aopts.enableWebtransport, true); + // Ok we have wt support + webtransportSupport.resolve(); + } catch (error) { + webtransportSupport.reject(error); + } +}); + +clientSession.onstream = mustCall((stream) => { + stream.onheaders = mustCall((stream) => { + // Well this should not happen on client side + assert.strictEqual(false, true); + }); +}); + +await clientSession.opened; +await webtransportSupport.promise; +// Now we open a webtransport session, which is actually +// a special bidirectional stream +const wtSessionStream = await clientSession.createBidirectionalStream({ + body: '', +}); +wtSessionStream.sendHeaders({ + ':method': 'CONNECT', + ':scheme': 'https', + // This one depends on draft, draft14 says "webtransport", draft15 says "webtransport-h3" + ':protocol': 'webtransport', + ':path': '/testwtpath', + ':authority': 'testserver:' + serverEndpoint.address.port +}, { + webtransport: true // Tell nghttp3 to treat the stream as a WT session stream +}); + +await serverSessionOpened.promise; +await sleep(500); +await wtSessionStream.closeWebtransportSessionStream(200, 'all perfect'); + +try { + await wtSessionStream.closed; +} catch (error) { + assert.strictEqual(error.errorCode, 200n); + assert.strictEqual(error.reason, 'all perfect'); +} +await clientSession.close(); +await serverEndpoint.close(); From e9c2303fb2f1fda88c1df15da16a36ddf29b2010 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 2 Aug 2026 17:59:23 +0200 Subject: [PATCH 19/37] quic: remove stop sending as it breaks firefox and chromium --- deps/ngtcp2/nghttp3/lib/nghttp3_conn.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c index e8df764a16d5..141904708244 100644 --- a/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c +++ b/deps/ngtcp2/nghttp3/lib/nghttp3_conn.c @@ -3751,7 +3751,10 @@ int nghttp3_conn_close_wt_session(nghttp3_conn *conn, int64_t session_id, stream->rstate.state = NGHTTP3_REQ_STREAM_STATE_IGN_REST; - return conn_call_stop_sending(conn, stream, NGHTTP3_WT_SESSION_GONE); + + return 0; + // turn it off for now as it set up firefox and chromium + // return conn_call_stop_sending(conn, stream, NGHTTP3_WT_SESSION_GONE); } int nghttp3_conn_on_wt_stream(nghttp3_conn *conn, nghttp3_stream *stream, From 1ef32d61f46ef6a7b090519342c030cb498d8e51 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 2 Aug 2026 21:44:33 +0200 Subject: [PATCH 20/37] quic: Fix test --- test/parallel/test-quic-h3-wt-session.mjs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/parallel/test-quic-h3-wt-session.mjs b/test/parallel/test-quic-h3-wt-session.mjs index 018efe7277c2..726a804a8fb7 100644 --- a/test/parallel/test-quic-h3-wt-session.mjs +++ b/test/parallel/test-quic-h3-wt-session.mjs @@ -3,7 +3,7 @@ // Test: HTTP/3 webtransport session establishment // Verifies that a session established and properly closed -import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; import assert from 'node:assert'; import * as fixtures from '../common/fixtures.mjs'; @@ -22,6 +22,9 @@ const serverSessionOpened = Promise.withResolvers(); const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall(async (stream) => { + console.log('Does nothing but is required') + }); ss.onapplication = mustCall((aopts) => { assert.strictEqual(!aopts.enableDatagrams, false); }); @@ -38,6 +41,7 @@ const serverEndpoint = await listen(mustCall(async (ss) => { transportParams: { maxDatagramFrameSize: 100 }, onheaders: mustCall(function(headers) { try { + console.log('onheaders1') assert.strictEqual(headers[':scheme'], 'https'); assert.strictEqual(headers[':method'], 'CONNECT'); assert.strictEqual(headers[':protocol'], 'webtransport'); // depends on the draft @@ -53,6 +57,7 @@ const serverEndpoint = await listen(mustCall(async (ss) => { } // Should only be installed on wt streams this.onwtsessionclose = mustCall((code, reason) => { + console.log('SESSIONCLOSE') assert.strictEqual(code, 200); assert.strictEqual(reason, 'all perfect'); this.session.close(); @@ -86,20 +91,23 @@ clientSession.onapplication = mustCall((aopts) => { } }); -clientSession.onstream = mustCall((stream) => { - stream.onheaders = mustCall((stream) => { +clientSession.onstream = mustNotCall((stream) => { + stream.onheaders = mustNotCall((stream) => { // Well this should not happen on client side - assert.strictEqual(false, true); }); }); +console.log('mark 1') await clientSession.opened; +console.log('mark 2') await webtransportSupport.promise; +console.log('mark 3') // Now we open a webtransport session, which is actually // a special bidirectional stream const wtSessionStream = await clientSession.createBidirectionalStream({ body: '', }); +console.log('mark 4') wtSessionStream.sendHeaders({ ':method': 'CONNECT', ':scheme': 'https', @@ -110,16 +118,18 @@ wtSessionStream.sendHeaders({ }, { webtransport: true // Tell nghttp3 to treat the stream as a WT session stream }); - +console.log('mark 5') await serverSessionOpened.promise; +console.log('mark 5a') await sleep(500); await wtSessionStream.closeWebtransportSessionStream(200, 'all perfect'); - +console.log('mark 6') try { await wtSessionStream.closed; } catch (error) { assert.strictEqual(error.errorCode, 200n); assert.strictEqual(error.reason, 'all perfect'); } + await clientSession.close(); await serverEndpoint.close(); From 9ead258cbc827af8934c1a96b4b7f3b342dd0f0e Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Mon, 3 Aug 2026 08:28:54 +0200 Subject: [PATCH 21/37] quic: Remove debug messages from test --- test/parallel/test-quic-h3-wt-session.mjs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/test/parallel/test-quic-h3-wt-session.mjs b/test/parallel/test-quic-h3-wt-session.mjs index 726a804a8fb7..fb61e321adac 100644 --- a/test/parallel/test-quic-h3-wt-session.mjs +++ b/test/parallel/test-quic-h3-wt-session.mjs @@ -23,7 +23,7 @@ const serverSessionOpened = Promise.withResolvers(); const serverEndpoint = await listen(mustCall(async (ss) => { ss.onstream = mustCall(async (stream) => { - console.log('Does nothing but is required') + console.log('Does nothing but is required'); }); ss.onapplication = mustCall((aopts) => { assert.strictEqual(!aopts.enableDatagrams, false); @@ -41,7 +41,6 @@ const serverEndpoint = await listen(mustCall(async (ss) => { transportParams: { maxDatagramFrameSize: 100 }, onheaders: mustCall(function(headers) { try { - console.log('onheaders1') assert.strictEqual(headers[':scheme'], 'https'); assert.strictEqual(headers[':method'], 'CONNECT'); assert.strictEqual(headers[':protocol'], 'webtransport'); // depends on the draft @@ -57,7 +56,6 @@ const serverEndpoint = await listen(mustCall(async (ss) => { } // Should only be installed on wt streams this.onwtsessionclose = mustCall((code, reason) => { - console.log('SESSIONCLOSE') assert.strictEqual(code, 200); assert.strictEqual(reason, 'all perfect'); this.session.close(); @@ -94,20 +92,17 @@ clientSession.onapplication = mustCall((aopts) => { clientSession.onstream = mustNotCall((stream) => { stream.onheaders = mustNotCall((stream) => { // Well this should not happen on client side + console.log('Called forbidden onheaders!'); }); }); -console.log('mark 1') await clientSession.opened; -console.log('mark 2') await webtransportSupport.promise; -console.log('mark 3') // Now we open a webtransport session, which is actually // a special bidirectional stream const wtSessionStream = await clientSession.createBidirectionalStream({ body: '', }); -console.log('mark 4') wtSessionStream.sendHeaders({ ':method': 'CONNECT', ':scheme': 'https', @@ -118,12 +113,9 @@ wtSessionStream.sendHeaders({ }, { webtransport: true // Tell nghttp3 to treat the stream as a WT session stream }); -console.log('mark 5') await serverSessionOpened.promise; -console.log('mark 5a') await sleep(500); await wtSessionStream.closeWebtransportSessionStream(200, 'all perfect'); -console.log('mark 6') try { await wtSessionStream.closed; } catch (error) { From d36414b6fbcb3d1ef13334cc0a10d99151d0606f Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Mon, 3 Aug 2026 08:29:28 +0200 Subject: [PATCH 22/37] quic: Add test bidi echo from client --- .../test-quic-h3-wt-bidi-from-client.mjs | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 test/parallel/test-quic-h3-wt-bidi-from-client.mjs diff --git a/test/parallel/test-quic-h3-wt-bidi-from-client.mjs b/test/parallel/test-quic-h3-wt-bidi-from-client.mjs new file mode 100644 index 000000000000..1c3e38819eb4 --- /dev/null +++ b/test/parallel/test-quic-h3-wt-bidi-from-client.mjs @@ -0,0 +1,219 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 webtransport session establishment +// Verifies that a session established and properly closed + +import { hasQuic, skip, mustCall, mustNotCall, mustCallAtLeast } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { drainableProtocol: dp } = await import('stream/iter'); + +const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); +const cert = fixtures.readKey('agent1-cert.pem'); + +const chunkSizes = [60000, 12, 1000000, 50000, 1600, 20000, 1000000, 30000, 0, 100]; +const numChunks = chunkSizes.length; +const byteLength = chunkSizes.reduce((accumulator, currentValue) => accumulator + currentValue, 0); + + +// Build a deterministic payload so we can verify integrity. +function buildChunk(index) { + const chunk = new Uint8Array(chunkSizes[index]); + // Fill with a pattern derived from the chunk index. + const val = index & 0xff; + for (let i = 0; i < chunkSizes[index]; i++) { + chunk[i] = (val + i) & 0xff; + } + return chunk; +} + +function checksum(data) { + let sum = 0; + for (let i = 0; i < data.byteLength; i++) { + sum = (sum + data[i]) | 0; + } + return sum; +} + +// Compute expected checksum. +let expectedChecksum = 0; +for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + expectedChecksum = (expectedChecksum + checksum(chunk)) | 0; +} + +const serverSessionOpened = Promise.withResolvers(); + + +const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustNotCall(async (stream) => { + console.log('Does nothing but is required! Fix me!'); + }); + ss.onapplication = mustCall((aopts) => { + assert.strictEqual(!aopts.enableDatagrams, false); + }); + ss.onhandshake = mustCall(function() { + assert.strictEqual(BigInt(this?.remoteTransportParams?.maxDatagramFrameSize) > 0, true); + }); + ss.onstream = mustCallAtLeast((stream) => { + stream.onsessionid = mustCallAtLeast(async (sessionid) => { + // Deinstall handlers + stream.onheaders = undefined; + stream.onsessionid = undefined; + const sessionid2 = await serverSessionOpened.promise; + assert.strictEqual(sessionid, sessionid2); + // Now we can echo all data. + const writer = stream.writer; + for await (const chunks of stream) { + for (const chunk of chunks) { + while (!writer.writeSync(chunk)) { + // Flow controlled — wait for drain before retrying. + const drainable = writer[dp](); + if (drainable) await drainable; + } + } + } + writer.end(); + await stream.closed; + }, stream.id !== 0n ? 1 : 0); // The second stream is a datastream + }, 2); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + application: { + enableConnectProtocol: true, + enableDatagrams: true, + enableWebtransport: true + }, + transportParams: { maxDatagramFrameSize: 100 }, + onheaders: mustCall(function(headers) { + try { + assert.strictEqual(headers[':scheme'], 'https'); + assert.strictEqual(headers[':method'], 'CONNECT'); + assert.strictEqual(headers[':protocol'], 'webtransport'); // depends on the draft + assert.strictEqual(headers[':path'], '/testwtpath'); + // We could also check for wt-available protocols + this.sendHeaders( + { ':status': '200' }, + { terminal: false, webtransport: true } + ); + serverSessionOpened.resolve(this.id); + } catch (error) { + serverSessionOpened.reject(error); + } + // Should only be installed on wt streams + this.onwtsessionclose = mustCall((code, reason) => { + assert.strictEqual(code, 200); + assert.strictEqual(reason, 'all perfect'); + this.session.close(); + }); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + application: { + enableConnectProtocol: true, + enableDatagrams: true, + enableWebtransport: true + }, + transportParams: { maxDatagramFrameSize: 1000 }, +}); + +const webtransportSupport = Promise.withResolvers(); + +clientSession.onapplication = mustCall((aopts) => { + try { + // Test for webtransport support + assert.strictEqual(aopts.enableConnectProtocol, true); + assert.strictEqual(aopts.enableDatagrams, true); + assert.strictEqual(aopts.enableWebtransport, true); + // Ok we have wt support + webtransportSupport.resolve(); + } catch (error) { + webtransportSupport.reject(error); + } +}); + +clientSession.onstream = mustNotCall((stream) => { + stream.onheaders = mustNotCall((stream) => { + // Well this should not happen on client side + console.log('Called onheaders on the client side!'); + }); +}); + +await clientSession.opened; +await webtransportSupport.promise; +// Now we open a webtransport session, which is actually +// a special bidirectional stream +const wtSessionStream = await clientSession.createBidirectionalStream({ + body: '', +}); +wtSessionStream.sendHeaders({ + ':method': 'CONNECT', + ':scheme': 'https', + // This one depends on draft, draft14 says "webtransport", draft15 says "webtransport-h3" + ':protocol': 'webtransport', + ':path': '/testwtpath', + ':authority': 'testserver:' + serverEndpoint.address.port +}, { + webtransport: true // Tell nghttp3 to treat the stream as a WT session stream +}); + +// Well let's get a bidi stream and send something +const clientBidiStream = await clientSession.createBidirectionalStream({ + incremental: true, + webtransportSession: wtSessionStream // Associate it with the sessionStream +}); +// Next step send some data + + +const readFromStream = mustCallAtLeast(async (stream) => { + const readChunks = []; + for await (const chunks of stream) { + readChunks.push(...chunks); + } + const receivedBytes = readChunks.reduce((accu, curVal) => accu + curVal.byteLength, 0); + + assert.strictEqual(receivedBytes, byteLength); + let receivedChecksum = 0; + for (const chunk of readChunks) { + receivedChecksum = (receivedChecksum + checksum(chunk)) | 0; + } + assert.strictEqual(receivedChecksum, expectedChecksum); +}, 1); + + +const writeToStream = mustCallAtLeast(async (stream) => { + const w = stream.writer; + for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + while (!w.writeSync(chunk)) { + // Flow controlled — wait for drain before retrying. + const drainable = w[dp](); + if (drainable) await drainable; + } + } + w.endSync(); +}, 1); + +await serverSessionOpened.promise; + +await Promise.all([readFromStream(clientBidiStream), writeToStream(clientBidiStream)]); +await wtSessionStream.closeWebtransportSessionStream(200, 'all perfect'); + +try { + await wtSessionStream.closed; +} catch (error) { + assert.strictEqual(error.errorCode, 200n); + assert.strictEqual(error.reason, 'all perfect'); +} +await clientSession.close(); +await serverEndpoint.close(); From eecb3786c23029138c0e07ab6e45ea49c2696037 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 8 Aug 2026 17:32:06 +0200 Subject: [PATCH 23/37] quic: Fix session close error propagation --- lib/internal/quic/quic.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index e919395c98db..3e93a53b744e 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -2869,6 +2869,10 @@ class QuicStream { // Prefer an error staged by destroy() (the original object the caller // passed) over the error delivered by the native close callback. error = inner.destroyError ?? error; + if (error?.errorCode === 0x170D7B68n && inner.wtSessionCloseError) { + error = inner.wtSessionCloseError; + inner.wtSessionCloseError = undefined; + } if (error !== undefined) { inner.pendingClose.reject(error); inner.pendingStream.reject(error); @@ -2899,6 +2903,7 @@ class QuicStream { inner.session[kRemoveStream](this); inner.writer?.fail(error); inner.session = undefined; + inner.wtSessionCloseError = undefined; inner.pendingClose.reject = undefined; inner.pendingClose.resolve = undefined; inner.destroyError = undefined; @@ -3049,6 +3054,12 @@ class QuicStream { if (typeof inner.onwtsessionclose === 'function') { safeCallbackInvoke(inner.onwtsessionclose, this, errorcode, errormessage); } + inner.wtSessionCloseError = makeQuicError( + 'ERR_QUIC_APPLICATION_ERROR', + 'Webtransport error', + 'application', + errorcode ?? 0, + errormessage ?? ''); } [kTrailers]() { From e35b7edc8a35ebc81ea01a78c2d632da6183a90c Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 8 Aug 2026 17:34:40 +0200 Subject: [PATCH 24/37] quic: fix and add unidi wt test --- .../test-quic-h3-wt-bidi-from-client.mjs | 4 +- .../test-quic-h3-wt-unidi-from-client.mjs | 210 ++++++++++++++++++ 2 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-quic-h3-wt-unidi-from-client.mjs diff --git a/test/parallel/test-quic-h3-wt-bidi-from-client.mjs b/test/parallel/test-quic-h3-wt-bidi-from-client.mjs index 1c3e38819eb4..958d3ba94a3d 100644 --- a/test/parallel/test-quic-h3-wt-bidi-from-client.mjs +++ b/test/parallel/test-quic-h3-wt-bidi-from-client.mjs @@ -1,7 +1,7 @@ // Flags: --experimental-quic --experimental-stream-iter --no-warnings -// Test: HTTP/3 webtransport session establishment -// Verifies that a session established and properly closed +// Test: HTTP/3 webtransport client initiated bidi stream +// Client creates a bidi stream and send data that is echoed back to the client import { hasQuic, skip, mustCall, mustNotCall, mustCallAtLeast } from '../common/index.mjs'; import assert from 'node:assert'; diff --git a/test/parallel/test-quic-h3-wt-unidi-from-client.mjs b/test/parallel/test-quic-h3-wt-unidi-from-client.mjs new file mode 100644 index 000000000000..e69dcb0116cd --- /dev/null +++ b/test/parallel/test-quic-h3-wt-unidi-from-client.mjs @@ -0,0 +1,210 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 webtransport client initiated uni stream +// Client creates an uni stream and send data to the server + +import { hasQuic, skip, mustCall, mustNotCall, mustCallAtLeast } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { drainableProtocol: dp } = await import('stream/iter'); + +const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); +const cert = fixtures.readKey('agent1-cert.pem'); + +const chunkSizes = [60000, 12, 1000000, 50000, 1600, 20000, 1000000, 30000, 0, 100]; +const numChunks = chunkSizes.length; +const byteLength = chunkSizes.reduce((accumulator, currentValue) => accumulator + currentValue, 0); + + +// Build a deterministic payload so we can verify integrity. +function buildChunk(index) { + const chunk = new Uint8Array(chunkSizes[index]); + // Fill with a pattern derived from the chunk index. + const val = index & 0xff; + for (let i = 0; i < chunkSizes[index]; i++) { + chunk[i] = (val + i) & 0xff; + } + return chunk; +} + +function checksum(data) { + let sum = 0; + for (let i = 0; i < data.byteLength; i++) { + sum = (sum + data[i]) | 0; + } + return sum; +} + +// Compute expected checksum. +let expectedChecksum = 0; +for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + expectedChecksum = (expectedChecksum + checksum(chunk)) | 0; +} + +const serverSessionOpened = Promise.withResolvers(); +const serverSessionRead = Promise.withResolvers(); + +const readChunks = []; +let serverSessionStream; +const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustNotCall(async (stream) => { + console.log('Does nothing but is required! Fix me!'); + }); + ss.onapplication = mustCall((aopts) => { + assert.strictEqual(!aopts.enableDatagrams, false); + }); + ss.onhandshake = mustCall(function() { + assert.strictEqual(BigInt(this?.remoteTransportParams?.maxDatagramFrameSize) > 0, true); + }); + ss.onstream = mustCallAtLeast((stream) => { + stream.onsessionid = mustCallAtLeast(async (sessionid) => { + // Deinstall handlers + stream.onheaders = undefined; + stream.onsessionid = undefined; + stream.closed.catch((error) => { + assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE + }) + const sessionid2 = await serverSessionOpened.promise; + assert.strictEqual(sessionid, sessionid2); + // Now we get the data + for await (const chunks of stream) { + readChunks.push(...chunks); + } + const receivedBytes = readChunks.reduce((accu, curVal) => accu + curVal.byteLength, 0); + + assert.strictEqual(receivedBytes, byteLength); + let receivedChecksum = 0; + for (const chunk of readChunks) { + receivedChecksum = (receivedChecksum + checksum(chunk)) | 0; + } + assert.strictEqual(receivedChecksum, expectedChecksum); + serverSessionStream.closed.catch(mustCall((error) => { + assert.strictEqual(error.errorCode, 200n); + assert.strictEqual(error.reason, 'all perfect'); + })) + await serverSessionStream.closeWebtransportSessionStream(200, 'all perfect'); + }, stream.id !== 0n ? 1 : 0); // The second stream is a datastream + }, 2); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + application: { + enableConnectProtocol: true, + enableDatagrams: true, + enableWebtransport: true + }, + transportParams: { maxDatagramFrameSize: 100 }, + onheaders: mustCall(function(headers) { + try { + assert.strictEqual(headers[':scheme'], 'https'); + assert.strictEqual(headers[':method'], 'CONNECT'); + assert.strictEqual(headers[':protocol'], 'webtransport'); // depends on the draft + assert.strictEqual(headers[':path'], '/testwtpath'); + // We could also check for wt-available protocols + this.sendHeaders( + { ':status': '200' }, + { terminal: false, webtransport: true } + ); + serverSessionOpened.resolve(this.id); + serverSessionStream = this; + } catch (error) { + serverSessionOpened.reject(error); + } + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + application: { + enableConnectProtocol: true, + enableDatagrams: true, + enableWebtransport: true + }, + transportParams: { maxDatagramFrameSize: 1000 }, +}); + +const webtransportSupport = Promise.withResolvers(); + +clientSession.onapplication = mustCall((aopts) => { + try { + // Test for webtransport support + assert.strictEqual(aopts.enableConnectProtocol, true); + assert.strictEqual(aopts.enableDatagrams, true); + assert.strictEqual(aopts.enableWebtransport, true); + // Ok we have wt support + webtransportSupport.resolve(); + } catch (error) { + webtransportSupport.reject(error); + } +}); + +clientSession.onstream = mustNotCall((stream) => { + stream.onheaders = mustNotCall((stream) => { + // Well this should not happen on client side + console.log('Called onheaders on the client side!'); + }); +}); + +await clientSession.opened; +await webtransportSupport.promise; +// Now we open a webtransport session, which is actually +// a special unidirectional stream +const wtSessionStream = await clientSession.createBidirectionalStream({ + body: '', +}); +wtSessionStream.onwtsessionclose = mustCall((code, reason) => { + assert.strictEqual(code, 200); + assert.strictEqual(reason, 'all perfect'); +}); +wtSessionStream.sendHeaders({ + ':method': 'CONNECT', + ':scheme': 'https', + // This one depends on draft, draft14 says "webtransport", draft15 says "webtransport-h3" + ':protocol': 'webtransport', + ':path': '/testwtpath', + ':authority': 'testserver:' + serverEndpoint.address.port +}, { + webtransport: true // Tell nghttp3 to treat the stream as a WT session stream +}); + +// Well let's get a unidi stream and send something +const clientUnidiStream = await clientSession.createBidirectionalStream({ + incremental: true, + webtransportSession: wtSessionStream // Associate it with the sessionStream +}); + +clientUnidiStream.closed.catch((error) => { + assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE +}); +// Next step send some data +await serverSessionOpened.promise; + + +const w = clientUnidiStream.writer; +for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + while (!w.writeSync(chunk)) { + // Flow controlled — wait for drain before retrying. + const drainable = w[dp](); + if (drainable) await drainable; + } +} +w.endSync(); + +try { + await wtSessionStream.closed; +} catch (error) { + assert.strictEqual(error.errorCode, 200n); + assert.strictEqual(error.reason, 'all perfect'); +} + +await clientSession.close(); +await serverEndpoint.close(); From 68c143de84248eacead9d07198bbbb78cbbdc7cf Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 8 Aug 2026 17:35:05 +0200 Subject: [PATCH 25/37] quic: add wt nghttp3 error codes --- src/quic/data.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/quic/data.h b/src/quic/data.h index 9c92a30c1ddf..dfcb96582c9a 100644 --- a/src/quic/data.h +++ b/src/quic/data.h @@ -187,7 +187,11 @@ class Store final : public MemoryRetainer { V(H3_VERSION_FALLBACK) \ V(QPACK_DECOMPRESSION_FAILED) \ V(QPACK_ENCODER_STREAM_ERROR) \ - V(QPACK_DECODER_STREAM_ERROR) + V(QPACK_DECODER_STREAM_ERROR) \ + V(WT_BUFFERED_STREAM_REJECTED) \ + V(WT_SESSION_GONE) \ + V(WT_ALPN_ERROR) \ + V(WT_REQUIREMENTS_NOT_MET) class QuicError final : public MemoryRetainer { public: From 71957ff760e118976efca0b5e36aa9ec383b89a7 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 9 Aug 2026 07:21:08 +0200 Subject: [PATCH 26/37] quic: Fix typo in EnquePending Webtransport Stream --- src/quic/http3.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 886ceea482e7..2e22bba4c35e 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -610,7 +610,7 @@ class Http3ApplicationImpl final : public Session::Application { bool MakeWebtransportStream(Stream& stream, int64_t sessionid) override { if (stream.is_pending()) { Debug(&session(), - "Enqueing Webtransport Session strean for pending stream"); + "Enqueing Webtransport Session stream for pending stream"); auto& state = GetOrCreateStreamState(stream); state.pending_webtransport_session = sessionid; return true; From 4aeb504e0b9d9ecd6bc01ac8c312a79c971cdcf4 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 9 Aug 2026 07:44:17 +0200 Subject: [PATCH 27/37] quic: fix stream limits --- .../test-quic-h3-wt-bidi-from-client.mjs | 15 +++++++++----- .../test-quic-h3-wt-unidi-from-client.mjs | 20 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/test/parallel/test-quic-h3-wt-bidi-from-client.mjs b/test/parallel/test-quic-h3-wt-bidi-from-client.mjs index 958d3ba94a3d..714992b56062 100644 --- a/test/parallel/test-quic-h3-wt-bidi-from-client.mjs +++ b/test/parallel/test-quic-h3-wt-bidi-from-client.mjs @@ -53,9 +53,6 @@ const serverSessionOpened = Promise.withResolvers(); const serverEndpoint = await listen(mustCall(async (ss) => { - ss.onstream = mustNotCall(async (stream) => { - console.log('Does nothing but is required! Fix me!'); - }); ss.onapplication = mustCall((aopts) => { assert.strictEqual(!aopts.enableDatagrams, false); }); @@ -91,7 +88,11 @@ const serverEndpoint = await listen(mustCall(async (ss) => { enableDatagrams: true, enableWebtransport: true }, - transportParams: { maxDatagramFrameSize: 100 }, + transportParams: { + maxDatagramFrameSize: 1000, + initialMaxStreamsBidi: 100, // default value according to spec + initialMaxStreamsUni: 100, // especially important as limit default is 0 + }, onheaders: mustCall(function(headers) { try { assert.strictEqual(headers[':scheme'], 'https'); @@ -124,7 +125,11 @@ const clientSession = await connect(serverEndpoint.address, { enableDatagrams: true, enableWebtransport: true }, - transportParams: { maxDatagramFrameSize: 1000 }, + transportParams: { + maxDatagramFrameSize: 1000, + initialMaxStreamsBidi: 100, // default value according to spec + initialMaxStreamsUni: 100, // especially important as limit default is 0 + }, }); const webtransportSupport = Promise.withResolvers(); diff --git a/test/parallel/test-quic-h3-wt-unidi-from-client.mjs b/test/parallel/test-quic-h3-wt-unidi-from-client.mjs index e69dcb0116cd..bf8d6335a02e 100644 --- a/test/parallel/test-quic-h3-wt-unidi-from-client.mjs +++ b/test/parallel/test-quic-h3-wt-unidi-from-client.mjs @@ -55,9 +55,6 @@ const serverSessionRead = Promise.withResolvers(); const readChunks = []; let serverSessionStream; const serverEndpoint = await listen(mustCall(async (ss) => { - ss.onstream = mustNotCall(async (stream) => { - console.log('Does nothing but is required! Fix me!'); - }); ss.onapplication = mustCall((aopts) => { assert.strictEqual(!aopts.enableDatagrams, false); }); @@ -100,7 +97,11 @@ const serverEndpoint = await listen(mustCall(async (ss) => { enableDatagrams: true, enableWebtransport: true }, - transportParams: { maxDatagramFrameSize: 100 }, + transportParams: { + maxDatagramFrameSize: 100, + initialMaxStreamsBidi: 100, // default value according to spec + initialMaxStreamsUni: 100, // especially important as limit default is 0 + }, onheaders: mustCall(function(headers) { try { assert.strictEqual(headers[':scheme'], 'https'); @@ -128,7 +129,11 @@ const clientSession = await connect(serverEndpoint.address, { enableDatagrams: true, enableWebtransport: true }, - transportParams: { maxDatagramFrameSize: 1000 }, + transportParams: { + maxDatagramFrameSize: 1000, + initialMaxStreamsBidi: 100, // default value according to spec + initialMaxStreamsUni: 100, // especially important as limit default is 0 + }, }); const webtransportSupport = Promise.withResolvers(); @@ -156,7 +161,7 @@ clientSession.onstream = mustNotCall((stream) => { await clientSession.opened; await webtransportSupport.promise; // Now we open a webtransport session, which is actually -// a special unidirectional stream +// a special bidirectional stream const wtSessionStream = await clientSession.createBidirectionalStream({ body: '', }); @@ -176,7 +181,7 @@ wtSessionStream.sendHeaders({ }); // Well let's get a unidi stream and send something -const clientUnidiStream = await clientSession.createBidirectionalStream({ +const clientUnidiStream = await clientSession.createUnidirectionalStream({ incremental: true, webtransportSession: wtSessionStream // Associate it with the sessionStream }); @@ -187,7 +192,6 @@ clientUnidiStream.closed.catch((error) => { // Next step send some data await serverSessionOpened.promise; - const w = clientUnidiStream.writer; for (let i = 0; i < numChunks; i++) { const chunk = buildChunk(i); From 2a533f717f7813b1a7bca3cd7bf298c1d662950f Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 9 Aug 2026 15:23:52 +0200 Subject: [PATCH 28/37] quic: add wt bidi from server test --- .../test-quic-h3-wt-bidi-from-server.mjs | 235 ++++++++++++++++++ .../test-quic-h3-wt-unidi-from-server.mjs | 219 ++++++++++++++++ 2 files changed, 454 insertions(+) create mode 100644 test/parallel/test-quic-h3-wt-bidi-from-server.mjs create mode 100644 test/parallel/test-quic-h3-wt-unidi-from-server.mjs diff --git a/test/parallel/test-quic-h3-wt-bidi-from-server.mjs b/test/parallel/test-quic-h3-wt-bidi-from-server.mjs new file mode 100644 index 000000000000..33d83e37c74f --- /dev/null +++ b/test/parallel/test-quic-h3-wt-bidi-from-server.mjs @@ -0,0 +1,235 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 webtransport client initiated bidi stream +// Server creates a bidi stream and client send data that is echoed back to the client + +import { hasQuic, skip, mustCall, mustNotCall, mustCallAtLeast } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { drainableProtocol: dp } = await import('stream/iter'); + +const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); +const cert = fixtures.readKey('agent1-cert.pem'); + +const chunkSizes = [60000, 12, 1000000, 50000, 1600, 20000, 1000000, 30000, 0, 100]; +const numChunks = chunkSizes.length; +const byteLength = chunkSizes.reduce((accumulator, currentValue) => accumulator + currentValue, 0); + + +// Build a deterministic payload so we can verify integrity. +function buildChunk(index) { + const chunk = new Uint8Array(chunkSizes[index]); + // Fill with a pattern derived from the chunk index. + const val = index & 0xff; + for (let i = 0; i < chunkSizes[index]; i++) { + chunk[i] = (val + i) & 0xff; + } + return chunk; +} + +function checksum(data) { + let sum = 0; + for (let i = 0; i < data.byteLength; i++) { + sum = (sum + data[i]) | 0; + } + return sum; +} + +// Compute expected checksum. +let expectedChecksum = 0; +for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + expectedChecksum = (expectedChecksum + checksum(chunk)) | 0; +} + +const serverSessionOpened = Promise.withResolvers(); + + +const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onapplication = mustCall((aopts) => { + assert.strictEqual(!aopts.enableDatagrams, false); + }); + ss.onhandshake = mustCall(function() { + assert.strictEqual(BigInt(this?.remoteTransportParams?.maxDatagramFrameSize) > 0, true); + }); + ss.onstream = mustCall((stream) => { + stream.onsessionid = mustNotCall(async (sessionid) => { + console.log('No client initiated stream expected!'); + }); + }); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + application: { + enableConnectProtocol: true, + enableDatagrams: true, + enableWebtransport: true + }, + transportParams: { + maxDatagramFrameSize: 1000, + initialMaxStreamsBidi: 100, // default value according to spec + initialMaxStreamsUni: 100, // especially important as limit default is 0 + }, + onheaders: mustCall(async function(headers) { + try { + assert.strictEqual(headers[':scheme'], 'https'); + assert.strictEqual(headers[':method'], 'CONNECT'); + assert.strictEqual(headers[':protocol'], 'webtransport'); // depends on the draft + assert.strictEqual(headers[':path'], '/testwtpath'); + // We could also check for wt-available protocols + this.sendHeaders( + { ':status': '200' }, + { terminal: false, webtransport: true } + ); + serverSessionOpened.resolve(this.id); + } catch (error) { + serverSessionOpened.reject(error); + } + // Should only be installed on wt streams + this.onwtsessionclose = mustCall((code, reason) => { + assert.strictEqual(code, 200); + assert.strictEqual(reason, 'all perfect'); + this.session.close(); + }); + + // Well let's get a bidi stream and echo all + const serverBidiStream = await this.session.createBidirectionalStream({ + incremental: true, + webtransportSession: this // Associate it with the sessionStream + }); + // Now we can echo all data. + const writer = serverBidiStream.writer; + let echoedData = 0; + for await (const chunks of serverBidiStream) { + for (const chunk of chunks) { + echoedData += chunk.byteLength + while (!writer.writeSync(chunk)) { + // Flow controlled — wait for drain before retrying. + const drainable = writer[dp](); + if (drainable) await drainable; + } + } + } + writer.end(); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + application: { + enableConnectProtocol: true, + enableDatagrams: true, + enableWebtransport: true + }, + transportParams: { + maxDatagramFrameSize: 1000, + initialMaxStreamsBidi: 100, // default value according to spec + initialMaxStreamsUni: 100, // especially important as limit default is 0 + }, +}); + +const webtransportSupport = Promise.withResolvers(); + +clientSession.onapplication = mustCall((aopts) => { + try { + // Test for webtransport support + assert.strictEqual(aopts.enableConnectProtocol, true); + assert.strictEqual(aopts.enableDatagrams, true); + assert.strictEqual(aopts.enableWebtransport, true); + // Ok we have wt support + webtransportSupport.resolve(); + } catch (error) { + webtransportSupport.reject(error); + } +}); + +clientSession.onstream = mustNotCall((stream) => { + stream.onheaders = mustNotCall((stream) => { + // Well this should not happen on client side + console.log('Called onheaders on the client side!'); + }); +}); + +await clientSession.opened; +await webtransportSupport.promise; + +clientSession.onstream = mustCall((stream) => { + stream.onsessionid = mustCall(async (sessionid) => { + assert.strictEqual(sessionid, wtSessionStream.id); + await Promise.all([readFromStream(stream), writeToStream(stream)]); + await wtSessionStream.closeWebtransportSessionStream(200, 'all perfect'); + }); +}); + +// Now we open a webtransport session, which is actually +// a special bidirectional stream +const wtSessionStream = await clientSession.createBidirectionalStream({ + body: '', +}); +wtSessionStream.sendHeaders({ + ':method': 'CONNECT', + ':scheme': 'https', + // This one depends on draft, draft14 says "webtransport", draft15 says "webtransport-h3" + ':protocol': 'webtransport', + ':path': '/testwtpath', + ':authority': 'testserver:' + serverEndpoint.address.port +}, { + webtransport: true // Tell nghttp3 to treat the stream as a WT session stream +}); + + +// Next step send some data + + +const readFromStream = mustCallAtLeast(async (stream) => { + const readChunks = []; + let readdata = 0; + for await (const chunks of stream) { + for (const chunk of chunks) { + readdata += chunk.byteLength; + } + readChunks.push(...chunks); + } + const receivedBytes = readChunks.reduce((accu, curVal) => accu + curVal.byteLength, 0); + + assert.strictEqual(receivedBytes, byteLength); + let receivedChecksum = 0; + for (const chunk of readChunks) { + receivedChecksum = (receivedChecksum + checksum(chunk)) | 0; + } + assert.strictEqual(receivedChecksum, expectedChecksum); +}, 1); + + +const writeToStream = mustCallAtLeast(async (stream) => { + const w = stream.writer; + let writtenData = 0; + for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + writtenData += chunk.byteLength; + while (!w.writeSync(chunk)) { + // Flow controlled — wait for drain before retrying. + const drainable = w[dp](); + if (drainable) await drainable; + } + } + w.endSync(); +}, 1); + +await serverSessionOpened.promise; + +try { + await wtSessionStream.closed; +} catch (error) { + assert.strictEqual(error.errorCode, 200n); + assert.strictEqual(error.reason, 'all perfect'); +} +await clientSession.close(); +await serverEndpoint.close(); diff --git a/test/parallel/test-quic-h3-wt-unidi-from-server.mjs b/test/parallel/test-quic-h3-wt-unidi-from-server.mjs new file mode 100644 index 000000000000..18c1934a1745 --- /dev/null +++ b/test/parallel/test-quic-h3-wt-unidi-from-server.mjs @@ -0,0 +1,219 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 webtransport client initiated uni stream +// Server creates an uni stream and send data to the client + +import { hasQuic, skip, mustCall, mustNotCall, mustCallAtLeast } from '../common/index.mjs'; +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { drainableProtocol: dp } = await import('stream/iter'); + +const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); +const cert = fixtures.readKey('agent1-cert.pem'); + +const chunkSizes = [60000, 12, 1000000, 50000, 1600, 20000, 1000000, 30000, 0, 100]; +const numChunks = chunkSizes.length; +const byteLength = chunkSizes.reduce((accumulator, currentValue) => accumulator + currentValue, 0); + + +// Build a deterministic payload so we can verify integrity. +function buildChunk(index) { + const chunk = new Uint8Array(chunkSizes[index]); + // Fill with a pattern derived from the chunk index. + const val = index & 0xff; + for (let i = 0; i < chunkSizes[index]; i++) { + chunk[i] = (val + i) & 0xff; + } + return chunk; +} + +function checksum(data) { + let sum = 0; + for (let i = 0; i < data.byteLength; i++) { + sum = (sum + data[i]) | 0; + } + return sum; +} + +// Compute expected checksum. +let expectedChecksum = 0; +for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + expectedChecksum = (expectedChecksum + checksum(chunk)) | 0; +} + +const serverSessionOpened = Promise.withResolvers(); +const serverSessionRead = Promise.withResolvers(); + +const readChunks = []; +let serverSessionStream; +const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onapplication = mustCall((aopts) => { + assert.strictEqual(!aopts.enableDatagrams, false); + }); + ss.onhandshake = mustCall(function() { + assert.strictEqual(BigInt(this?.remoteTransportParams?.maxDatagramFrameSize) > 0, true); + }); + ss.onstream = mustCallAtLeast((stream) => { + + }, 1); +}), { + sni: { '*': { keys: [key], certs: [cert] } }, + application: { + enableConnectProtocol: true, + enableDatagrams: true, + enableWebtransport: true + }, + transportParams: { + maxDatagramFrameSize: 100, + initialMaxStreamsBidi: 100, // default value according to spec + initialMaxStreamsUni: 100, // especially important as limit default is 0 + }, + onheaders: mustCall(async function(headers) { + try { + assert.strictEqual(headers[':scheme'], 'https'); + assert.strictEqual(headers[':method'], 'CONNECT'); + assert.strictEqual(headers[':protocol'], 'webtransport'); // depends on the draft + assert.strictEqual(headers[':path'], '/testwtpath'); + // We could also check for wt-available protocols + this.sendHeaders( + { ':status': '200' }, + { terminal: false, webtransport: true } + ); + serverSessionOpened.resolve(this.id); + serverSessionStream = this; + + serverSessionStream.onwtsessionclose = mustCall((code, reason) => { + assert.strictEqual(code, 200); + assert.strictEqual(reason, 'all perfect'); + }); + } catch (error) { + serverSessionOpened.reject(error); + } + // Well let's get a unidi stream and send something + const serverUnidiStream = await this.session.createUnidirectionalStream({ + incremental: true, + webtransportSession: serverSessionStream // Associate it with the sessionStream + }); + + serverUnidiStream.closed.catch((error) => { + assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE + }); + // Next step send some data + await serverSessionOpened.promise; + + const w = serverUnidiStream.writer; + for (let i = 0; i < numChunks; i++) { + const chunk = buildChunk(i); + while (!w.writeSync(chunk)) { + // Flow controlled — wait for drain before retrying. + const drainable = w[dp](); + if (drainable) await drainable; + } + } + w.endSync(); + }), +}); + +const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + application: { + enableConnectProtocol: true, + enableDatagrams: true, + enableWebtransport: true + }, + transportParams: { + maxDatagramFrameSize: 1000, + initialMaxStreamsBidi: 100, // default value according to spec + initialMaxStreamsUni: 100, // especially important as limit default is 0 + }, +}); + +const webtransportSupport = Promise.withResolvers(); + +clientSession.onapplication = mustCall((aopts) => { + try { + // Test for webtransport support + assert.strictEqual(aopts.enableConnectProtocol, true); + assert.strictEqual(aopts.enableDatagrams, true); + assert.strictEqual(aopts.enableWebtransport, true); + // Ok we have wt support + webtransportSupport.resolve(); + } catch (error) { + webtransportSupport.reject(error); + } +}); + +let wtSessionStream; + +clientSession.onstream = mustCall((stream) => { + stream.onheaders = mustNotCall((stream) => { + // Well this should not happen on client side + console.log('Called onheaders on the client side!'); + }); + stream.onsessionid = mustCallAtLeast(async (sessionid) => { + // Deinstall handlers + stream.onheaders = undefined; + stream.onsessionid = undefined; + stream.closed.catch((error) => { + assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE + }) + const sessionid2 = await serverSessionOpened.promise; + assert.strictEqual(sessionid, sessionid2); + // Now we get the data + for await (const chunks of stream) { + readChunks.push(...chunks); + } + const receivedBytes = readChunks.reduce((accu, curVal) => accu + curVal.byteLength, 0); + + assert.strictEqual(receivedBytes, byteLength); + let receivedChecksum = 0; + for (const chunk of readChunks) { + receivedChecksum = (receivedChecksum + checksum(chunk)) | 0; + } + assert.strictEqual(receivedChecksum, expectedChecksum); + wtSessionStream.closed.catch(mustCall((error) => { + assert.strictEqual(error.errorCode, 200n); + assert.strictEqual(error.reason, 'all perfect'); + })) + await wtSessionStream.closeWebtransportSessionStream(200, 'all perfect'); + }, stream.id !== 0n ? 1 : 0); // The second stream is a datastream +}); + +await clientSession.opened; +await webtransportSupport.promise; +// Now we open a webtransport session, which is actually +// a special bidirectional stream +wtSessionStream = await clientSession.createBidirectionalStream({ + body: '', +}); +wtSessionStream.sendHeaders({ + ':method': 'CONNECT', + ':scheme': 'https', + // This one depends on draft, draft14 says "webtransport", draft15 says "webtransport-h3" + ':protocol': 'webtransport', + ':path': '/testwtpath', + ':authority': 'testserver:' + serverEndpoint.address.port +}, { + webtransport: true // Tell nghttp3 to treat the stream as a WT session stream +}); + + + +try { + await wtSessionStream.closed; +} catch (error) { + assert.strictEqual(error.errorCode, 200n); + assert.strictEqual(error.reason, 'all perfect'); +} + +await clientSession.close(); +await serverEndpoint.close(); From c336ccd9ad293b8e0110ffd075fd35ac6b5e9854 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 15 Aug 2026 07:42:29 +0200 Subject: [PATCH 29/37] quic: Fix js lint --- lib/internal/quic/quic.js | 10 +-- lib/internal/quic/symbols.js | 1 + .../test-quic-h3-wt-bidi-from-client.mjs | 12 ++-- .../test-quic-h3-wt-bidi-from-server.mjs | 24 +++---- .../test-quic-h3-wt-unidi-from-client.mjs | 25 ++++--- .../test-quic-h3-wt-unidi-from-server.mjs | 70 +++++++++---------- 6 files changed, 66 insertions(+), 76 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 3e93a53b744e..1a7497a957c3 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -3055,11 +3055,11 @@ class QuicStream { safeCallbackInvoke(inner.onwtsessionclose, this, errorcode, errormessage); } inner.wtSessionCloseError = makeQuicError( - 'ERR_QUIC_APPLICATION_ERROR', - 'Webtransport error', - 'application', - errorcode ?? 0, - errormessage ?? ''); + 'ERR_QUIC_APPLICATION_ERROR', + 'Webtransport error', + 'application', + errorcode ?? 0, + errormessage ?? ''); } [kTrailers]() { diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index e660a2ba0164..d8d508c75fc1 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -42,6 +42,7 @@ const kHandshake = Symbol('kHandshake'); const kHandshakeCompleted = Symbol('kHandshakeCompleted'); const kVerifyPeer = Symbol('kVerifyPeer'); const kHeaders = Symbol('kHeaders'); +const kSessionId = Symbol('kSessionId'); const kKeylog = Symbol('kKeylog'); const kListen = Symbol('kListen'); const kQlog = Symbol('kQlog'); diff --git a/test/parallel/test-quic-h3-wt-bidi-from-client.mjs b/test/parallel/test-quic-h3-wt-bidi-from-client.mjs index 714992b56062..c42c2698f9c4 100644 --- a/test/parallel/test-quic-h3-wt-bidi-from-client.mjs +++ b/test/parallel/test-quic-h3-wt-bidi-from-client.mjs @@ -88,10 +88,10 @@ const serverEndpoint = await listen(mustCall(async (ss) => { enableDatagrams: true, enableWebtransport: true }, - transportParams: { + transportParams: { maxDatagramFrameSize: 1000, - initialMaxStreamsBidi: 100, // default value according to spec - initialMaxStreamsUni: 100, // especially important as limit default is 0 + initialMaxStreamsBidi: 100, // Default value according to spec + initialMaxStreamsUni: 100, // Especially important as limit default is 0 }, onheaders: mustCall(function(headers) { try { @@ -125,10 +125,10 @@ const clientSession = await connect(serverEndpoint.address, { enableDatagrams: true, enableWebtransport: true }, - transportParams: { + transportParams: { maxDatagramFrameSize: 1000, - initialMaxStreamsBidi: 100, // default value according to spec - initialMaxStreamsUni: 100, // especially important as limit default is 0 + initialMaxStreamsBidi: 100, // Default value according to spec + initialMaxStreamsUni: 100, // Especially important as limit default is 0 }, }); diff --git a/test/parallel/test-quic-h3-wt-bidi-from-server.mjs b/test/parallel/test-quic-h3-wt-bidi-from-server.mjs index 33d83e37c74f..24b44a45dbaf 100644 --- a/test/parallel/test-quic-h3-wt-bidi-from-server.mjs +++ b/test/parallel/test-quic-h3-wt-bidi-from-server.mjs @@ -61,8 +61,8 @@ const serverEndpoint = await listen(mustCall(async (ss) => { }); ss.onstream = mustCall((stream) => { stream.onsessionid = mustNotCall(async (sessionid) => { - console.log('No client initiated stream expected!'); - }); + console.log('No client initiated stream expected!'); + }); }); }), { sni: { '*': { keys: [key], certs: [cert] } }, @@ -71,10 +71,10 @@ const serverEndpoint = await listen(mustCall(async (ss) => { enableDatagrams: true, enableWebtransport: true }, - transportParams: { + transportParams: { maxDatagramFrameSize: 1000, - initialMaxStreamsBidi: 100, // default value according to spec - initialMaxStreamsUni: 100, // especially important as limit default is 0 + initialMaxStreamsBidi: 100, // Default value according to spec + initialMaxStreamsUni: 100, // Especially important as limit default is 0 }, onheaders: mustCall(async function(headers) { try { @@ -105,10 +105,8 @@ const serverEndpoint = await listen(mustCall(async (ss) => { }); // Now we can echo all data. const writer = serverBidiStream.writer; - let echoedData = 0; for await (const chunks of serverBidiStream) { for (const chunk of chunks) { - echoedData += chunk.byteLength while (!writer.writeSync(chunk)) { // Flow controlled — wait for drain before retrying. const drainable = writer[dp](); @@ -128,10 +126,10 @@ const clientSession = await connect(serverEndpoint.address, { enableDatagrams: true, enableWebtransport: true }, - transportParams: { + transportParams: { maxDatagramFrameSize: 1000, - initialMaxStreamsBidi: 100, // default value according to spec - initialMaxStreamsUni: 100, // especially important as limit default is 0 + initialMaxStreamsBidi: 100, // Default value according to spec + initialMaxStreamsUni: 100, // Especially important as limit default is 0 }, }); @@ -190,11 +188,7 @@ wtSessionStream.sendHeaders({ const readFromStream = mustCallAtLeast(async (stream) => { const readChunks = []; - let readdata = 0; for await (const chunks of stream) { - for (const chunk of chunks) { - readdata += chunk.byteLength; - } readChunks.push(...chunks); } const receivedBytes = readChunks.reduce((accu, curVal) => accu + curVal.byteLength, 0); @@ -210,10 +204,8 @@ const readFromStream = mustCallAtLeast(async (stream) => { const writeToStream = mustCallAtLeast(async (stream) => { const w = stream.writer; - let writtenData = 0; for (let i = 0; i < numChunks; i++) { const chunk = buildChunk(i); - writtenData += chunk.byteLength; while (!w.writeSync(chunk)) { // Flow controlled — wait for drain before retrying. const drainable = w[dp](); diff --git a/test/parallel/test-quic-h3-wt-unidi-from-client.mjs b/test/parallel/test-quic-h3-wt-unidi-from-client.mjs index bf8d6335a02e..7c97615e79e1 100644 --- a/test/parallel/test-quic-h3-wt-unidi-from-client.mjs +++ b/test/parallel/test-quic-h3-wt-unidi-from-client.mjs @@ -50,7 +50,6 @@ for (let i = 0; i < numChunks; i++) { } const serverSessionOpened = Promise.withResolvers(); -const serverSessionRead = Promise.withResolvers(); const readChunks = []; let serverSessionStream; @@ -66,9 +65,9 @@ const serverEndpoint = await listen(mustCall(async (ss) => { // Deinstall handlers stream.onheaders = undefined; stream.onsessionid = undefined; - stream.closed.catch((error) => { + stream.closed.catch(mustCall((error) => { assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE - }) + })); const sessionid2 = await serverSessionOpened.promise; assert.strictEqual(sessionid, sessionid2); // Now we get the data @@ -86,7 +85,7 @@ const serverEndpoint = await listen(mustCall(async (ss) => { serverSessionStream.closed.catch(mustCall((error) => { assert.strictEqual(error.errorCode, 200n); assert.strictEqual(error.reason, 'all perfect'); - })) + })); await serverSessionStream.closeWebtransportSessionStream(200, 'all perfect'); }, stream.id !== 0n ? 1 : 0); // The second stream is a datastream }, 2); @@ -97,10 +96,10 @@ const serverEndpoint = await listen(mustCall(async (ss) => { enableDatagrams: true, enableWebtransport: true }, - transportParams: { + transportParams: { maxDatagramFrameSize: 100, - initialMaxStreamsBidi: 100, // default value according to spec - initialMaxStreamsUni: 100, // especially important as limit default is 0 + initialMaxStreamsBidi: 100, // Default value according to spec + initialMaxStreamsUni: 100, // Especially important as limit default is 0 }, onheaders: mustCall(function(headers) { try { @@ -129,10 +128,10 @@ const clientSession = await connect(serverEndpoint.address, { enableDatagrams: true, enableWebtransport: true }, - transportParams: { + transportParams: { maxDatagramFrameSize: 1000, - initialMaxStreamsBidi: 100, // default value according to spec - initialMaxStreamsUni: 100, // especially important as limit default is 0 + initialMaxStreamsBidi: 100, // Default value according to spec + initialMaxStreamsUni: 100, // Especially important as limit default is 0 }, }); @@ -186,9 +185,9 @@ const clientUnidiStream = await clientSession.createUnidirectionalStream({ webtransportSession: wtSessionStream // Associate it with the sessionStream }); -clientUnidiStream.closed.catch((error) => { - assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE -}); +clientUnidiStream.closed.catch(mustCall((error) => { + assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE +})); // Next step send some data await serverSessionOpened.promise; diff --git a/test/parallel/test-quic-h3-wt-unidi-from-server.mjs b/test/parallel/test-quic-h3-wt-unidi-from-server.mjs index 18c1934a1745..01e014752dc4 100644 --- a/test/parallel/test-quic-h3-wt-unidi-from-server.mjs +++ b/test/parallel/test-quic-h3-wt-unidi-from-server.mjs @@ -50,7 +50,6 @@ for (let i = 0; i < numChunks; i++) { } const serverSessionOpened = Promise.withResolvers(); -const serverSessionRead = Promise.withResolvers(); const readChunks = []; let serverSessionStream; @@ -62,7 +61,7 @@ const serverEndpoint = await listen(mustCall(async (ss) => { assert.strictEqual(BigInt(this?.remoteTransportParams?.maxDatagramFrameSize) > 0, true); }); ss.onstream = mustCallAtLeast((stream) => { - + console.log('Does nothing but is required'); }, 1); }), { sni: { '*': { keys: [key], certs: [cert] } }, @@ -71,10 +70,10 @@ const serverEndpoint = await listen(mustCall(async (ss) => { enableDatagrams: true, enableWebtransport: true }, - transportParams: { + transportParams: { maxDatagramFrameSize: 100, - initialMaxStreamsBidi: 100, // default value according to spec - initialMaxStreamsUni: 100, // especially important as limit default is 0 + initialMaxStreamsBidi: 100, // Default value according to spec + initialMaxStreamsUni: 100, // Especially important as limit default is 0 }, onheaders: mustCall(async function(headers) { try { @@ -103,9 +102,9 @@ const serverEndpoint = await listen(mustCall(async (ss) => { webtransportSession: serverSessionStream // Associate it with the sessionStream }); - serverUnidiStream.closed.catch((error) => { + serverUnidiStream.closed.catch(mustCall((error) => { assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE - }); + })); // Next step send some data await serverSessionOpened.promise; @@ -130,10 +129,10 @@ const clientSession = await connect(serverEndpoint.address, { enableDatagrams: true, enableWebtransport: true }, - transportParams: { + transportParams: { maxDatagramFrameSize: 1000, - initialMaxStreamsBidi: 100, // default value according to spec - initialMaxStreamsUni: 100, // especially important as limit default is 0 + initialMaxStreamsBidi: 100, // Default value according to spec + initialMaxStreamsUni: 100, // Especially important as limit default is 0 }, }); @@ -160,32 +159,32 @@ clientSession.onstream = mustCall((stream) => { console.log('Called onheaders on the client side!'); }); stream.onsessionid = mustCallAtLeast(async (sessionid) => { - // Deinstall handlers - stream.onheaders = undefined; - stream.onsessionid = undefined; - stream.closed.catch((error) => { - assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE - }) - const sessionid2 = await serverSessionOpened.promise; - assert.strictEqual(sessionid, sessionid2); - // Now we get the data - for await (const chunks of stream) { - readChunks.push(...chunks); - } - const receivedBytes = readChunks.reduce((accu, curVal) => accu + curVal.byteLength, 0); + // Deinstall handlers + stream.onheaders = undefined; + stream.onsessionid = undefined; + stream.closed.catch(mustCall((error) => { + assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE + })); + const sessionid2 = await serverSessionOpened.promise; + assert.strictEqual(sessionid, sessionid2); + // Now we get the data + for await (const chunks of stream) { + readChunks.push(...chunks); + } + const receivedBytes = readChunks.reduce((accu, curVal) => accu + curVal.byteLength, 0); - assert.strictEqual(receivedBytes, byteLength); - let receivedChecksum = 0; - for (const chunk of readChunks) { - receivedChecksum = (receivedChecksum + checksum(chunk)) | 0; - } - assert.strictEqual(receivedChecksum, expectedChecksum); - wtSessionStream.closed.catch(mustCall((error) => { - assert.strictEqual(error.errorCode, 200n); - assert.strictEqual(error.reason, 'all perfect'); - })) - await wtSessionStream.closeWebtransportSessionStream(200, 'all perfect'); - }, stream.id !== 0n ? 1 : 0); // The second stream is a datastream + assert.strictEqual(receivedBytes, byteLength); + let receivedChecksum = 0; + for (const chunk of readChunks) { + receivedChecksum = (receivedChecksum + checksum(chunk)) | 0; + } + assert.strictEqual(receivedChecksum, expectedChecksum); + wtSessionStream.closed.catch(mustCall((error) => { + assert.strictEqual(error.errorCode, 200n); + assert.strictEqual(error.reason, 'all perfect'); + })); + await wtSessionStream.closeWebtransportSessionStream(200, 'all perfect'); + }, stream.id !== 0n ? 1 : 0); // The second stream is a datastream }); await clientSession.opened; @@ -207,7 +206,6 @@ wtSessionStream.sendHeaders({ }); - try { await wtSessionStream.closed; } catch (error) { From f3bb48bca9e1e9c5436cc89bd5b82ec4eb0d5035 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 15 Aug 2026 09:29:07 +0200 Subject: [PATCH 30/37] quic: update writedesiredsize on maxdataframe This commit will react on a callback from ngtcp2, when a maxdata frame arrives and update the scheduled streams accordingly. A PR to get this callback is pending. Signed-off-by: Marten Richter --- src/quic/application.cc | 11 ++- src/quic/application.h | 8 ++ src/quic/http3.cc | 10 +++ src/quic/session.cc | 20 ++++- ...uic-h3-maxdata-external-buffer-failure.mjs | 76 +++++++++++++++++++ 5 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-quic-h3-maxdata-external-buffer-failure.mjs diff --git a/src/quic/application.cc b/src/quic/application.cc index 0530118edd4c..69a377c7158d 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -417,7 +417,7 @@ class DefaultApplication final : public Session::Application { void BlockStream(stream_id id) override { if (auto stream = session().FindStream(id)) [[likely]] { // Remove the stream from the send queue. It will be re-scheduled - // via ExtendMaxStreamData when the peer grants more flow control. + // via ExtendMax(Stream)Data when the peer grants more flow control. // Without this, SendPendingData would repeatedly pop and retry // the same blocked stream in an infinite loop. stream->Unschedule(); @@ -433,6 +433,15 @@ class DefaultApplication final : public Session::Application { stream->Schedule(&stream_queue_); } + void ExtendMaxData(uint64_t max_data) override { + // The peer granted more flow control for session. Re-schedule + // all streams so SendPendingData will resume writing. + for (auto& [id, stream] : session().streams()) { + stream->Schedule(&stream_queue_); + } + } + + bool StreamCommit(Session::StreamData* stream_data, size_t datalen) override { DCHECK_NOT_NULL(stream_data); CHECK(stream_data->stream); diff --git a/src/quic/application.h b/src/quic/application.h index ace39e9e6749..a22ad3254623 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -142,6 +142,14 @@ class Session::Application : public MemoryRetainer { // By default do nothing. } + // Called when the Session determines that the flow control window for the + // session has been expanded. Not all Application types will require + // this notification so the default is to do nothing. + virtual void ExtendMaxData(uint64_t max_data) { + Debug(session_, "Application extending max data"); + // By default do nothing. + } + // Different Applications may wish to set some application data in the // session ticket (e.g. http/3 would set server settings in the application // data). The first byte written MUST be the Application::Type enum value. diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 2e22bba4c35e..0e7dbb7a41ad 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -389,6 +389,16 @@ class Http3ApplicationImpl final : public Session::Application { nghttp3_conn_unblock_stream(*this, stream->id()); } + void ExtendMaxData(uint64_t max_data) override { + Debug(&session(), + "HTTP/3 application extending max data to %" PRIu64, + max_data); + for (auto& [id, stream] : session().streams()) { + stream->UpdateWriteDesiredSize(); // the stream might be blocked on js side + // is unblock stream also required? + } + } + void CollectSessionTicketAppData( SessionTicket::AppData* app_data) const override { uint8_t buf[kSessionTicketAppDataSize]; diff --git a/src/quic/session.cc b/src/quic/session.cc index 90d59487a2a7..1cd38bdf3e1b 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -1359,6 +1359,14 @@ struct Session::Impl final : public MemoryRetainer { return NGTCP2_SUCCESS; } + static int on_extend_max_data(ngtcp2_conn* conn, + uint64_t max_data, + void* user_data) { + NGTCP2_CALLBACK_SCOPE(session) + session->application().ExtendMaxData(max_data); + return NGTCP2_SUCCESS; + } + static int on_get_new_cid(ngtcp2_conn* conn, ngtcp2_cid* cid, ngtcp2_stateless_reset_token* token, @@ -1705,8 +1713,12 @@ struct Session::Impl final : public MemoryRetainer { ngtcp2_crypto_get_path_challenge_data2_cb, on_receive_stream_stop_sending, #ifdef NGTCP2_CALLBACKS_V5 - nullptr, // stream_close2 + nullptr, +#ifdef NGTCP2_CALLBACKS_V6 + on_extend_max_data, #endif +#endif // NGTCP2_CALLBACKS_V5 +#endif // NGTCP2_CALLBACKS_V4 }; static constexpr ngtcp2_callbacks SERVER = { @@ -1757,8 +1769,12 @@ struct Session::Impl final : public MemoryRetainer { ngtcp2_crypto_get_path_challenge_data2_cb, on_receive_stream_stop_sending, #ifdef NGTCP2_CALLBACKS_V5 - nullptr, // stream_close2 + nullptr, +#ifdef NGTCP2_CALLBACKS_V6 + on_extend_max_data, #endif +#endif // NGTCP2_CALLBACKS_V5 +#endif // NGTCP2_CALLBACKS_V4 }; }; diff --git a/test/parallel/test-quic-h3-maxdata-external-buffer-failure.mjs b/test/parallel/test-quic-h3-maxdata-external-buffer-failure.mjs new file mode 100644 index 000000000000..e93a2cf173a8 --- /dev/null +++ b/test/parallel/test-quic-h3-maxdata-external-buffer-failure.mjs @@ -0,0 +1,76 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: Quic maxdata updates on http/3 +// Client sends a body that precisely fills the session window size, +// and verifies that it is data transfer is not stalled. + +import { hasQuic, skip } from '../common/index.mjs'; +import { readFile } from 'node:fs/promises'; +import { setTimeout as sleep } from 'node:timers/promises'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { drainableProtocol } = await import('stream/iter'); + +const keys = 'test/fixtures/keys'; +const key = createPrivateKey(await readFile(`${keys}/agent1-key.pem`)); +const cert = await readFile(`${keys}/agent1-cert.pem`); + +const WINDOW = 4096; +// Fills the window exactly: +// considers all framing including some initial session capsules +const BODY = WINDOW - 38; + +let letServerRead; +const serverMayRead = new Promise((resolve) => { letServerRead = resolve; }); + +const endpoint = await listen((session) => { + session.onstream = async (stream) => { + await serverMayRead; + // eslint-disable-next-line no-unused-vars + for await (const _ of stream) { /* reading extends the window */ } + }; +}, { + sni: { '*': { keys: [key], certs: [cert] } }, + transportParams: { + initialMaxStreamDataBidiRemote: 1024 * 1024, // Make sure maxstreamdata does not block + initialMaxData: WINDOW, + }, + onheaders() { this.sendHeaders({ ':status': '200' }); }, +}); + +const session = await connect(endpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', +}); +await session.opened; + +// Budget well above the window, so the window is what stops the writer. +const stream = await session.createBidirectionalStream({ budget: 1024 * 1024 }); +stream.sendHeaders({ + ':method': 'POST', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', +}, { terminal: false }); + +const writer = stream.writer; +writer.writeSync(new Uint8Array(BODY)); + +// Long enough for every byte to be acked. The peer acks as data arrives, +// whether or not its application has read any of it, so by now the window is +// exhausted, the send buffer is empty, and no further ACK can arrive. +await sleep(500); + +const watchdog = setTimeout(() => { + console.error('STALLED: no drain after MAX_STREAM_DATA'); + process.exit(1); +}, 5000); +letServerRead(); // Extend the window, with no ack attached +await writer[drainableProtocol](); + +clearTimeout(watchdog); +process.exit(0); From c64f8545098e405a039edf9f02ab7b12f365dd69 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 16 Aug 2026 20:11:11 +0200 Subject: [PATCH 31/37] quic: add option waitUntilAvailable to creating streams --- doc/api/quic.md | 10 ++++ lib/internal/quic/quic.js | 9 ++-- lib/internal/quic/symbols.js | 1 - src/quic/application.cc | 1 - src/quic/http3.cc | 4 +- src/quic/session.cc | 26 +++++++-- src/quic/session.h | 3 ++ .../test-quic-stream-limits-pending.mjs | 54 +++++++++++++------ 8 files changed, 79 insertions(+), 29 deletions(-) diff --git a/doc/api/quic.md b/doc/api/quic.md index 5bdac8189fac..89772c2cceff 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -1356,6 +1356,11 @@ added: v23.8.0 will buffer before `writeSync()` returns `false`. When the buffered data exceeds this limit, the caller should wait for drain before writing more. **Default:** `65536` (64 KB). + * `waitUntilAvailable` {boolean} When true the promise will wait until flow + control will allow to open the stream. If set to false, the function + will fail synchronously, if flow control will not allow to open the stream + immediately. + **Default:** `false` * `onheaders` {Function} Callback for received initial response headers. Called with `(headers)`. * `ontrailers` {Function} Callback for received trailing headers. @@ -1397,6 +1402,11 @@ added: v23.8.0 will buffer before `writeSync()` returns `false`. When the buffered data exceeds this limit, the caller should wait for drain before writing more. **Default:** `65536` (64 KB). + * `waitUntilAvailable` {boolean} When true the promise will wait until flow + control will allow to open the stream. If set to false, the function + will fail synchronously, if flow control will not allow to open the stream + immediately. + **Default:** `false` * `onheaders` {Function} Callback for received initial response headers. Called with `(headers)`. * `ontrailers` {Function} Callback for received trailing headers. diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 1a7497a957c3..216f5915bcba 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -2869,9 +2869,9 @@ class QuicStream { // Prefer an error staged by destroy() (the original object the caller // passed) over the error delivered by the native close callback. error = inner.destroyError ?? error; - if (error?.errorCode === 0x170D7B68n && inner.wtSessionCloseError) { - error = inner.wtSessionCloseError; - inner.wtSessionCloseError = undefined; + if (error?.errorCode === 0x170D7B68n && inner.wtSessionCloseError) { + error = inner.wtSessionCloseError; + inner.wtSessionCloseError = undefined; } if (error !== undefined) { inner.pendingClose.reject(error); @@ -3730,6 +3730,7 @@ class QuicSession { webtransport = false, webtransportSession = undefined, headers, + waitUntilAvailable = false, onheaders, onsessionid, onwtsessionclose, @@ -3766,7 +3767,7 @@ class QuicSession { } } - const handle = this.#handle.openStream(direction, validatedBody); + const handle = this.#handle.openStream(direction, waitUntilAvailable, validatedBody); if (handle === undefined) { throw new ERR_QUIC_OPEN_STREAM_FAILED(); } diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index d8d508c75fc1..e660a2ba0164 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -42,7 +42,6 @@ const kHandshake = Symbol('kHandshake'); const kHandshakeCompleted = Symbol('kHandshakeCompleted'); const kVerifyPeer = Symbol('kVerifyPeer'); const kHeaders = Symbol('kHeaders'); -const kSessionId = Symbol('kSessionId'); const kKeylog = Symbol('kKeylog'); const kListen = Symbol('kListen'); const kQlog = Symbol('kQlog'); diff --git a/src/quic/application.cc b/src/quic/application.cc index 69a377c7158d..a2511568026f 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -440,7 +440,6 @@ class DefaultApplication final : public Session::Application { stream->Schedule(&stream_queue_); } } - bool StreamCommit(Session::StreamData* stream_data, size_t datalen) override { DCHECK_NOT_NULL(stream_data); diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 0e7dbb7a41ad..260746655158 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -394,8 +394,8 @@ class Http3ApplicationImpl final : public Session::Application { "HTTP/3 application extending max data to %" PRIu64, max_data); for (auto& [id, stream] : session().streams()) { - stream->UpdateWriteDesiredSize(); // the stream might be blocked on js side - // is unblock stream also required? + stream->UpdateWriteDesiredSize(); // the stream might be blocked + // on js side, is unblock stream also required? } } diff --git a/src/quic/session.cc b/src/quic/session.cc index 1cd38bdf3e1b..850af53d74cf 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -1168,17 +1168,26 @@ struct Session::Impl final : public MemoryRetainer { } DCHECK(args[0]->IsUint32()); + DCHECK(args[1]->IsBoolean()); + + auto direction = FromV8Value(args[0]); + if (!args[1].As()->Value()) { // This is waitUntilAvailable + if (!session->CanImmediatelyOpenStream(direction)) { + return THROW_ERR_INVALID_STATE(env, + "No new stream available" + " within flow control"); + } + } // GetDataQueueFromSource handles type validation. std::shared_ptr data_source; - if (!Stream::GetDataQueueFromSource(env, args[1]).To(&data_source)) + if (!Stream::GetDataQueueFromSource(env, args[2]).To(&data_source)) [[unlikely]] { return THROW_ERR_INVALID_ARG_VALUE(env, "Invalid data source"); } session->impl_->handshake_deferred_ = false; SendPendingDataScope send_scope(session); - auto direction = FromV8Value(args[0]); Local stream; if (session->OpenStream(direction, std::move(data_source)).ToLocal(&stream)) [[likely]] { @@ -3219,6 +3228,14 @@ BaseObjectPtr Session::CreateStream( return {}; } +bool Session::CanImmediatelyOpenStream(Direction direction) { + if (direction == Direction::BIDIRECTIONAL) { + return max_local_streams_bidi() > 0; + } else { + return max_local_streams_uni() > 0; + } +} + MaybeLocal Session::OpenStream(Direction direction, std::shared_ptr data_source) { // If can_create_streams() returns false, we are not able to open a stream @@ -3524,13 +3541,12 @@ void Session::SetApplicationError(error_code app_error_code) { uint64_t Session::max_local_streams_uni() const { DCHECK(!is_destroyed()); - return ngtcp2_conn_get_streams_uni_left(*this); + return ngtcp2_conn_get_streams_uni_left2(*this); } uint64_t Session::max_local_streams_bidi() const { DCHECK(!is_destroyed()); - return ngtcp2_conn_get_local_transport_params(*this) - ->initial_max_streams_bidi; + return ngtcp2_conn_get_streams_bidi_left2(*this); } void Session::set_wrapped() { diff --git a/src/quic/session.h b/src/quic/session.h index 3ab3503b2dd4..a167a4f5d988 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -537,6 +537,9 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { size_t max_packet_size() const; void set_priority_supported(bool on = true); + // Check whether flow control permits opening another stream + bool CanImmediatelyOpenStream(Direction direction); + // Open a new locally-initialized stream with the specified directionality. // If the session is not yet in a state where the stream can be opened -- // such as when the handshake is not yet sufficiently far along and ORTT diff --git a/test/parallel/test-quic-stream-limits-pending.mjs b/test/parallel/test-quic-stream-limits-pending.mjs index 2d81b0d79096..ebe4e71f7ac7 100644 --- a/test/parallel/test-quic-stream-limits-pending.mjs +++ b/test/parallel/test-quic-stream-limits-pending.mjs @@ -15,22 +15,30 @@ if (!hasQuic) { const { listen, connect } = await import('../common/quic.mjs'); const { bytes } = await import('stream/iter'); +const { setTimeout: sleep } = await import('timers/promises'); const encoder = new TextEncoder(); const allDone = Promise.withResolvers(); +const twoDone = Promise.withResolvers(); let serverStreamCount = 0; // Server allows only 1 bidi stream at a time. const serverEndpoint = await listen(mustCall((serverSession) => { serverSession.onstream = mustCall(async (stream) => { - await bytes(stream); + const streambytes = await bytes(stream); stream.writer.endSync(); await stream.closed; - if (++serverStreamCount === 2) { - serverSession.close(); + ++serverStreamCount; + if (serverStreamCount === 2) { + twoDone.resolve(); + } + if (serverStreamCount === 3) { allDone.resolve(); } - }, 2); + if (serverStreamCount === 4) { + serverSession.close(); + } + }, 4); }), { transportParams: { initialMaxStreamsBidi: 1 }, }); @@ -43,6 +51,7 @@ let opened = 0; // First stream opens immediately (within the limit). const s1 = await clientSession.createBidirectionalStream({ body: encoder.encode('stream 1'), + waitUntilAvailable: true }); // eslint-disable-next-line node-core/must-call-assert @@ -50,11 +59,15 @@ s1.opened.then(() => { opened++; }); -// Second stream is created but queued as pending because the -// server only allows 1 concurrent bidi stream. -const s2 = await clientSession.createBidirectionalStream({ - body: encoder.encode('stream 2'), -}); +try { + // Second stream should not open, but throw. + const s2 = await clientSession.createBidirectionalStream({ + body: encoder.encode('stream 2'), + waitUntilAvailable: false + }); +} catch (error) { + assert.strictEqual(error.code, 'ERR_INVALID_STATE'); +} // eslint-disable-next-line node-core/must-call-assert s2.opened.then(() => { @@ -65,12 +78,13 @@ s2.opened.then(() => { // server only allows 1 concurrent bidi stream. const s3 = await clientSession.createBidirectionalStream({ body: encoder.encode('stream 3'), + waitUntilAvailable: true }); -// s2 should be pending until s1 closes and the server grants +// s3 should be pending until s1 closes and the server grants // more stream credits. -assert.strictEqual(s2.pending, true); +assert.strictEqual(s3.pending, true); assert.strictEqual(opened, 1); // Drain and close the first stream. @@ -85,12 +99,20 @@ await Promise.all([assert.rejects(s3.opened, err), assert.rejects(s3.closed, err // After s1 closes, the server sends MAX_STREAMS which opens s2. // Wait for the server to receive both streams. -await allDone.promise; +await twoDone.promise; assert.strictEqual(opened, 2); - -// s2 should no longer be pending. -for await (const _ of s2) { /* drain */ } // eslint-disable-line no-unused-vars -await s2.closed; +// s3 should no longer be pending. +for await (const _ of s3) { /* drain */ } // eslint-disable-line no-unused-vars +await s3.closed; + +await sleep(10); // we wait a bit, as we do not have a callback exposed to js +// fourth stream should open immediately and not throw +const s4 = await clientSession.createBidirectionalStream({ + body: encoder.encode('stream 4'), + waitUntilAvailable: false +}); +await s4.closed; +await allDone.promise; await clientSession.close(); await serverEndpoint.close(); From 062c52b8893b1dfb890be6bf3a320f59cd8028cd Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 16 Aug 2026 20:14:05 +0200 Subject: [PATCH 32/37] quic: fix two wt tests, due to changes in main --- test/parallel/test-quic-h3-wt-unidi-from-client.mjs | 6 ------ test/parallel/test-quic-h3-wt-unidi-from-server.mjs | 7 +------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/test/parallel/test-quic-h3-wt-unidi-from-client.mjs b/test/parallel/test-quic-h3-wt-unidi-from-client.mjs index 7c97615e79e1..184c4aeb0dc0 100644 --- a/test/parallel/test-quic-h3-wt-unidi-from-client.mjs +++ b/test/parallel/test-quic-h3-wt-unidi-from-client.mjs @@ -65,9 +65,6 @@ const serverEndpoint = await listen(mustCall(async (ss) => { // Deinstall handlers stream.onheaders = undefined; stream.onsessionid = undefined; - stream.closed.catch(mustCall((error) => { - assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE - })); const sessionid2 = await serverSessionOpened.promise; assert.strictEqual(sessionid, sessionid2); // Now we get the data @@ -185,9 +182,6 @@ const clientUnidiStream = await clientSession.createUnidirectionalStream({ webtransportSession: wtSessionStream // Associate it with the sessionStream }); -clientUnidiStream.closed.catch(mustCall((error) => { - assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE -})); // Next step send some data await serverSessionOpened.promise; diff --git a/test/parallel/test-quic-h3-wt-unidi-from-server.mjs b/test/parallel/test-quic-h3-wt-unidi-from-server.mjs index 01e014752dc4..650f02905a74 100644 --- a/test/parallel/test-quic-h3-wt-unidi-from-server.mjs +++ b/test/parallel/test-quic-h3-wt-unidi-from-server.mjs @@ -102,9 +102,6 @@ const serverEndpoint = await listen(mustCall(async (ss) => { webtransportSession: serverSessionStream // Associate it with the sessionStream }); - serverUnidiStream.closed.catch(mustCall((error) => { - assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE - })); // Next step send some data await serverSessionOpened.promise; @@ -162,9 +159,7 @@ clientSession.onstream = mustCall((stream) => { // Deinstall handlers stream.onheaders = undefined; stream.onsessionid = undefined; - stream.closed.catch(mustCall((error) => { - assert.strictEqual(error.errorCode, 0x170D7B68n); // NGHTTP3_WT_SESSION_GONE - })); + const sessionid2 = await serverSessionOpened.promise; assert.strictEqual(sessionid, sessionid2); // Now we get the data From a8e318f28d3af69cb643cd654bdf0fef9d142013 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Mon, 31 Aug 2026 12:30:15 +0200 Subject: [PATCH 33/37] quic_ fix WtSessionclose --- src/quic/http3.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 260746655158..8d722df5f989 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -983,7 +983,7 @@ class Http3ApplicationImpl final : public Session::Application { const uint8_t* msg, size_t msglen) { auto* state = GetStreamState(stream); - if (!env()->can_call_into_js() || !state->wants_wtsessionclose) return; + if (!env()->can_call_into_js()) return; CallbackScope cb_scope(&stream); auto& binding = BindingData::Get(env()); Local argv[] = { From 16728aca6e3e38187f9e9bb3cb4c6107b09ac989 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 6 Sep 2026 19:22:00 +0200 Subject: [PATCH 34/37] quic: remove debug code and formatting --- src/quic/session.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/quic/session.cc b/src/quic/session.cc index 850af53d74cf..56e6876c98f7 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -1174,8 +1174,8 @@ struct Session::Impl final : public MemoryRetainer { if (!args[1].As()->Value()) { // This is waitUntilAvailable if (!session->CanImmediatelyOpenStream(direction)) { return THROW_ERR_INVALID_STATE(env, - "No new stream available" - " within flow control"); + "No new stream available within flow control" + ); } } @@ -3546,7 +3546,7 @@ uint64_t Session::max_local_streams_uni() const { uint64_t Session::max_local_streams_bidi() const { DCHECK(!is_destroyed()); - return ngtcp2_conn_get_streams_bidi_left2(*this); + return ngtcp2_conn_get_streams_bidi_left2(*this); } void Session::set_wrapped() { From 414dd97ffeb448aeac100c97f94bd77332a39ed3 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 26 Sep 2026 19:09:34 +0200 Subject: [PATCH 35/37] quic: some fixes after rebase --- lib/internal/quic/quic.js | 9 ++-- src/quic/bindingdata.cc | 4 ++ src/quic/http3.cc | 1 - src/quic/session.cc | 5 +- .../test-quic-internal-setcallbacks.mjs | 2 + .../test-quic-stream-limits-pending.mjs | 54 +++++++++---------- 6 files changed, 39 insertions(+), 36 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 216f5915bcba..784607d8d764 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -52,7 +52,7 @@ const { sendHeaders, setCallbacks, setHeadersInterest, - setSessionIdInterest, + setWebtransportInterest, makeWebtransportStream, closeWebtransportSessionStream, @@ -1380,9 +1380,10 @@ function updateHeaderInterest(handle, inner) { function updateWebtransportInterest(handle, inner) { if (handle === undefined) return; - setSessionIdInterest( + setWebtransportInterest( handle, - inner.onsessionid !== undefined || inner.onwtsessionclose !== undefined, + inner.onsessionid !== undefined, + inner.onwtsessionclose !== undefined, ); } @@ -3730,7 +3731,7 @@ class QuicSession { webtransport = false, webtransportSession = undefined, headers, - waitUntilAvailable = false, + waitUntilAvailable = true, onheaders, onsessionid, onwtsessionclose, diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index 4b03a11fffc1..e5461a2c727e 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -298,6 +298,10 @@ void BindingData::InitPerContext(Realm* realm, Local target) { target, "setWebtransportInterest", SetWebtransportInterest); + SetMethod(realm->context(), + target, + "closeWebtransportSessionStream", + CloseWebtransportSessionStream); SetMethod(realm->context(), target, "makeWebtransportStream", diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 8d722df5f989..64eb854aee79 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -982,7 +982,6 @@ class Http3ApplicationImpl final : public Session::Application { uint32_t wt_error_code, const uint8_t* msg, size_t msglen) { - auto* state = GetStreamState(stream); if (!env()->can_call_into_js()) return; CallbackScope cb_scope(&stream); auto& binding = BindingData::Get(env()); diff --git a/src/quic/session.cc b/src/quic/session.cc index 56e6876c98f7..d52130892d54 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -1174,8 +1174,7 @@ struct Session::Impl final : public MemoryRetainer { if (!args[1].As()->Value()) { // This is waitUntilAvailable if (!session->CanImmediatelyOpenStream(direction)) { return THROW_ERR_INVALID_STATE(env, - "No new stream available within flow control" - ); + "No new stream available within flow control"); } } @@ -1725,7 +1724,6 @@ struct Session::Impl final : public MemoryRetainer { nullptr, #ifdef NGTCP2_CALLBACKS_V6 on_extend_max_data, -#endif #endif // NGTCP2_CALLBACKS_V5 #endif // NGTCP2_CALLBACKS_V4 }; @@ -1781,7 +1779,6 @@ struct Session::Impl final : public MemoryRetainer { nullptr, #ifdef NGTCP2_CALLBACKS_V6 on_extend_max_data, -#endif #endif // NGTCP2_CALLBACKS_V5 #endif // NGTCP2_CALLBACKS_V4 }; diff --git a/test/parallel/test-quic-internal-setcallbacks.mjs b/test/parallel/test-quic-internal-setcallbacks.mjs index 910f1fa97a74..2cd3688378ef 100644 --- a/test/parallel/test-quic-internal-setcallbacks.mjs +++ b/test/parallel/test-quic-internal-setcallbacks.mjs @@ -32,9 +32,11 @@ const callbacks = { onStreamClose() {}, onStreamDrain() {}, onStreamReset() {}, + onStreamSessionId() {}, onStreamStopSending() {}, onStreamHeaders() {}, onStreamTrailers() {}, + onStreamWTSessionClose() {}, }; // Fail if any callback is missing for (const fn of Object.keys(callbacks)) { diff --git a/test/parallel/test-quic-stream-limits-pending.mjs b/test/parallel/test-quic-stream-limits-pending.mjs index ebe4e71f7ac7..5180c6c73ca6 100644 --- a/test/parallel/test-quic-stream-limits-pending.mjs +++ b/test/parallel/test-quic-stream-limits-pending.mjs @@ -25,10 +25,14 @@ let serverStreamCount = 0; // Server allows only 1 bidi stream at a time. const serverEndpoint = await listen(mustCall((serverSession) => { serverSession.onstream = mustCall(async (stream) => { - const streambytes = await bytes(stream); + console.log("server mark 1") + await bytes(stream); + console.log("server mark 2") stream.writer.endSync(); + console.log("server mark 3") await stream.closed; ++serverStreamCount; + console.log('serverStreamCount', serverStreamCount) if (serverStreamCount === 2) { twoDone.resolve(); } @@ -38,7 +42,7 @@ const serverEndpoint = await listen(mustCall((serverSession) => { if (serverStreamCount === 4) { serverSession.close(); } - }, 4); + }, 3); }), { transportParams: { initialMaxStreamsBidi: 1 }, }); @@ -51,7 +55,6 @@ let opened = 0; // First stream opens immediately (within the limit). const s1 = await clientSession.createBidirectionalStream({ body: encoder.encode('stream 1'), - waitUntilAvailable: true }); // eslint-disable-next-line node-core/must-call-assert @@ -59,30 +62,29 @@ s1.opened.then(() => { opened++; }); -try { - // Second stream should not open, but throw. - const s2 = await clientSession.createBidirectionalStream({ - body: encoder.encode('stream 2'), - waitUntilAvailable: false - }); -} catch (error) { - assert.strictEqual(error.code, 'ERR_INVALID_STATE'); -} - -// eslint-disable-next-line node-core/must-call-assert -s2.opened.then(() => { - opened++; -}); - +await assert.rejects( + async () => { + // Second stream should not open, but throw. + await clientSession.createBidirectionalStream({ + body: encoder.encode('stream 2'), + waitUntilAvailable: false, + }); + // eslint-disable-next-line node-core/must-call-assert + s2.opened.then(() => { + opened++; + }); + }, + { + name: 'Error', + message: 'No new stream available within flow control', + }, +); // Third stream is created but queued as pending because the // server only allows 1 concurrent bidi stream. const s3 = await clientSession.createBidirectionalStream({ body: encoder.encode('stream 3'), - waitUntilAvailable: true }); - - -// s3 should be pending until s1 closes and the server grants +// Note, s3 should be pending until s1 closes and the server grants // more stream credits. assert.strictEqual(s3.pending, true); assert.strictEqual(opened, 1); @@ -96,7 +98,6 @@ s3.destroy(err); await Promise.all([assert.rejects(s3.opened, err), assert.rejects(s3.closed, err)]); - // After s1 closes, the server sends MAX_STREAMS which opens s2. // Wait for the server to receive both streams. await twoDone.promise; @@ -105,14 +106,13 @@ assert.strictEqual(opened, 2); for await (const _ of s3) { /* drain */ } // eslint-disable-line no-unused-vars await s3.closed; -await sleep(10); // we wait a bit, as we do not have a callback exposed to js +await sleep(10); // We wait a bit, as we do not have a callback exposed to js // fourth stream should open immediately and not throw const s4 = await clientSession.createBidirectionalStream({ body: encoder.encode('stream 4'), waitUntilAvailable: false }); -await s4.closed; -await allDone.promise; +await Promise.all([s4.closed, allDone.promise]); await clientSession.close(); -await serverEndpoint.close(); +await serverEndpoint.close(); \ No newline at end of file From abcec7139014ecfd936c21693b6364d4b33f2b3d Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 27 Sep 2026 09:50:36 +0200 Subject: [PATCH 36/37] quic: Fix badly rebased test --- .../test-quic-stream-limits-pending.mjs | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/test/parallel/test-quic-stream-limits-pending.mjs b/test/parallel/test-quic-stream-limits-pending.mjs index 5180c6c73ca6..76220df8ad5b 100644 --- a/test/parallel/test-quic-stream-limits-pending.mjs +++ b/test/parallel/test-quic-stream-limits-pending.mjs @@ -25,14 +25,10 @@ let serverStreamCount = 0; // Server allows only 1 bidi stream at a time. const serverEndpoint = await listen(mustCall((serverSession) => { serverSession.onstream = mustCall(async (stream) => { - console.log("server mark 1") await bytes(stream); - console.log("server mark 2") stream.writer.endSync(); - console.log("server mark 3") await stream.closed; ++serverStreamCount; - console.log('serverStreamCount', serverStreamCount) if (serverStreamCount === 2) { twoDone.resolve(); } @@ -62,11 +58,12 @@ s1.opened.then(() => { opened++; }); +let s2; await assert.rejects( async () => { // Second stream should not open, but throw. - await clientSession.createBidirectionalStream({ - body: encoder.encode('stream 2'), + s2 = await clientSession.createBidirectionalStream({ + body: encoder.encode('stream 2a'), waitUntilAvailable: false, }); // eslint-disable-next-line node-core/must-call-assert @@ -79,13 +76,26 @@ await assert.rejects( message: 'No new stream available within flow control', }, ); +// Ok try again a second second stream, that patiently waits +s2 = await clientSession.createBidirectionalStream({ + body: encoder.encode('stream 2b') +}); +// eslint-disable-next-line node-core/must-call-assert +s2.opened.then(() => { + opened++; +}); + + // Third stream is created but queued as pending because the // server only allows 1 concurrent bidi stream. const s3 = await clientSession.createBidirectionalStream({ body: encoder.encode('stream 3'), }); -// Note, s3 should be pending until s1 closes and the server grants + + +// s2 and s3 should be pending until s1 closes and the server grants // more stream credits. +assert.strictEqual(s2.pending, true); assert.strictEqual(s3.pending, true); assert.strictEqual(opened, 1); @@ -98,13 +108,12 @@ s3.destroy(err); await Promise.all([assert.rejects(s3.opened, err), assert.rejects(s3.closed, err)]); -// After s1 closes, the server sends MAX_STREAMS which opens s2. +// After s1 closes, the server sends MAX_STREAMS which opens s3. // Wait for the server to receive both streams. await twoDone.promise; -assert.strictEqual(opened, 2); -// s3 should no longer be pending. -for await (const _ of s3) { /* drain */ } // eslint-disable-line no-unused-vars -await s3.closed; +// s2 should no longer be pending. +for await (const _ of s2) { /* drain */ } // eslint-disable-line no-unused-vars +await s2.closed; await sleep(10); // We wait a bit, as we do not have a callback exposed to js // fourth stream should open immediately and not throw @@ -115,4 +124,4 @@ const s4 = await clientSession.createBidirectionalStream({ await Promise.all([s4.closed, allDone.promise]); await clientSession.close(); -await serverEndpoint.close(); \ No newline at end of file +await serverEndpoint.close(); From c34d679f95a2b68b9e4ce0d95c9a6b1781ffdbc3 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 27 Sep 2026 16:09:41 +0200 Subject: [PATCH 37/37] quic: minor edit --- src/quic/http3.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 64eb854aee79..d1dcff0941ae 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -1502,7 +1502,7 @@ class Http3ApplicationImpl final : public Session::Application { auto& session = app.session(); - // DATA frames for a request stream the application already destroyed can + // DATA frames for a wt 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