@@ -184,6 +184,8 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) {
184184#define SESSION_JS_METHODS (V ) \
185185 V (Destroy, destroy, SIDE_EFFECT ) \
186186 V (GetRemoteAddress, getRemoteAddress, NO_SIDE_EFFECT ) \
187+ V (GetServername, getServername, NO_SIDE_EFFECT ) \
188+ V (GetAlpnProtocol, getAlpnProtocol, NO_SIDE_EFFECT ) \
187189 V (GetLocalAddress, getLocalAddress, NO_SIDE_EFFECT ) \
188190 V (GetCertificate, getCertificate, NO_SIDE_EFFECT ) \
189191 V (GetEphemeralKeyInfo, getEphemeralKey, NO_SIDE_EFFECT ) \
@@ -781,6 +783,8 @@ struct Session::Impl final : public MemoryRetainer {
781783 SocketAddress remote_address_;
782784 std::unique_ptr<Application> application_;
783785 StreamsMap streams_;
786+ // Emits deferred until after session setup is completed
787+ std::vector<std::function<void ()>> deferred_emits_;
784788 TimerWrapHandle timer_;
785789 size_t send_scope_depth_ = 0 ;
786790 QuicError last_error_;
@@ -1001,6 +1005,36 @@ struct Session::Impl final : public MemoryRetainer {
10011005 session->Destroy ();
10021006 }
10031007
1008+ // The SNI servername from the TLS handshake; empty (-> undefined) only if
1009+ // none was sent.
1010+ JS_METHOD (GetServername) {
1011+ auto env = Environment::GetCurrent (args);
1012+ Session* session;
1013+ ASSIGN_OR_RETURN_UNWRAP (&session, args.This ());
1014+ if (session->is_destroyed ()) return ;
1015+ auto sn = session->tls_session ().servername ();
1016+ if (sn.empty ()) return ;
1017+ Local<Value> ret;
1018+ if (ToV8Value (env->context (), sn).ToLocal (&ret)) {
1019+ args.GetReturnValue ().Set (ret);
1020+ }
1021+ }
1022+
1023+ // The negotiated ALPN protocol. Undefined only for clients before the
1024+ // handshake is completed, as ALPN is mandatory for QUIC.
1025+ JS_METHOD (GetAlpnProtocol) {
1026+ auto env = Environment::GetCurrent (args);
1027+ Session* session;
1028+ ASSIGN_OR_RETURN_UNWRAP (&session, args.This ());
1029+ if (session->is_destroyed ()) return ;
1030+ auto proto = session->tls_session ().protocol ();
1031+ if (proto.empty ()) return ;
1032+ Local<Value> ret;
1033+ if (ToV8Value (env->context (), proto).ToLocal (&ret)) {
1034+ args.GetReturnValue ().Set (ret);
1035+ }
1036+ }
1037+
10041038 JS_METHOD (GetRemoteAddress) {
10051039 auto env = Environment::GetCurrent (args);
10061040 Session* session;
@@ -2190,6 +2224,12 @@ const Session::Options& Session::options() const {
21902224void Session::EmitQlog (uint32_t flags, std::string_view data) {
21912225 if (!env ()->can_call_into_js ()) return ;
21922226
2227+ if (!is_destroyed () && must_defer_emits ()) {
2228+ QueueDeferredEmit (
2229+ [this , flags, held = std::string (data)]() { EmitQlog (flags, held); });
2230+ return ;
2231+ }
2232+
21932233 bool fin = (flags & NGTCP2_QLOG_WRITE_FLAG_FIN ) != 0 ;
21942234
21952235 // Fun fact... ngtcp2 does not emit the final qlog statement until the
@@ -2312,6 +2352,16 @@ bool Session::ReadPacket(const uint8_t* data,
23122352 // Process deferred operations that couldn't run inside callback
23132353 // scopes (e.g., HTTP/3 GOAWAY handling that calls into JS).
23142354 application ().PostReceive ();
2355+ // Surface a server session to JS once its ClientHello has been
2356+ // processed (OnSelectAlpn fired: SNI + ALPN are known and reliable).
2357+ // Held first-flight events - including 0-RTT request streams - replay
2358+ // at emit. The !wrapped guard makes this fire exactly once, on
2359+ // whichever packet completes the ClientHello (so a multi-datagram
2360+ // ClientHello is handled correctly).
2361+ if (is_server () && hello_processed_ && !impl_->state ()->wrapped &&
2362+ !is_destroyed ()) {
2363+ endpoint ().EmitNewSession (BaseObjectPtr<Session>(this ));
2364+ }
23152365 }
23162366 return true ;
23172367 }
@@ -2975,6 +3025,30 @@ void Session::set_wrapped() {
29753025 impl_->state ()->wrapped = 1 ;
29763026}
29773027
3028+ bool Session::must_defer_emits () const {
3029+ // Server sessions are surfaced to JS (via the deferred new-session emit)
3030+ // only after the ClientHello has been processed and wrapped; anything
3031+ // emitted before then has no JS wrapper to receive it and must be held
3032+ // for replay.
3033+ return is_server () && !impl_->state ()->wrapped ;
3034+ }
3035+
3036+ void Session::QueueDeferredEmit (std::function<void ()> fn) {
3037+ impl_->deferred_emits_ .emplace_back (std::move (fn));
3038+ }
3039+
3040+ void Session::ReplayDeferredEmits () {
3041+ if (is_destroyed ()) return ;
3042+ DCHECK (impl_->state ()->wrapped );
3043+ // Runs synchronously immediately after the new-session callback
3044+ // returns (still within first-flight processing).
3045+ auto emits = std::move (impl_->deferred_emits_ );
3046+ for (auto & emit : emits) {
3047+ if (is_destroyed ()) return ;
3048+ emit ();
3049+ }
3050+ }
3051+
29783052void Session::set_priority_supported (bool on) {
29793053 DCHECK (!is_destroyed ());
29803054 impl_->state ()->priority_supported = on ? 1 : 0 ;
@@ -3284,6 +3358,10 @@ bool Session::HandshakeCompleted() {
32843358
32853359 Debug (this , " Session handshake completed" );
32863360 impl_->state ()->handshake_completed = 1 ;
3361+ // This implies fully completing a handshake without setting hello_processed
3362+ // (set during ALPN negotiation). Should be impossible unless ALPN flow is
3363+ // changed drastically, but good to check as it'd lose sessions.
3364+ DCHECK (!is_server () || hello_processed_);
32873365
32883366 STAT_RECORD_TIMESTAMP (Stats, handshake_completed_at);
32893367 SetStreamOpenAllowed ();
@@ -3482,6 +3560,7 @@ void Session::set_max_datagram_size(uint16_t size) {
34823560
34833561void Session::EmitGoaway (stream_id last_stream_id) {
34843562 if (is_destroyed ()) return ;
3563+ if (DeferEmit ([this , last_stream_id] { EmitGoaway (last_stream_id); })) return ;
34853564 if (!env ()->can_call_into_js ()) return ;
34863565
34873566 CallbackScope<Session> cb_scope (this );
@@ -3496,6 +3575,14 @@ void Session::EmitGoaway(stream_id last_stream_id) {
34963575
34973576void Session::EmitDatagram (Store&& datagram, DatagramReceivedFlags flag) {
34983577 DCHECK (!is_destroyed ());
3578+
3579+ if (must_defer_emits ()) {
3580+ QueueDeferredEmit ([this , datagram = std::move (datagram), flag]() mutable {
3581+ EmitDatagram (std::move (datagram), flag);
3582+ });
3583+ return ;
3584+ }
3585+
34993586 if (!env ()->can_call_into_js ()) return ;
35003587
35013588 CallbackScope<Session> cbv_scope (this );
@@ -3511,6 +3598,8 @@ void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlags flag) {
35113598void Session::EmitDatagramStatus (datagram_id id, quic::DatagramStatus status) {
35123599 DCHECK (!is_destroyed ());
35133600
3601+ if (DeferEmit ([this , id, status] { EmitDatagramStatus (id, status); })) return ;
3602+
35143603 if (!env ()->can_call_into_js ()) return ;
35153604
35163605 CallbackScope<Session> cb_scope (this );
@@ -3672,6 +3761,7 @@ void Session::EmitSessionTicket(Store&& ticket) {
36723761
36733762void Session::EmitApplication () {
36743763 if (is_destroyed ()) return ;
3764+ if (DeferEmit ([this ] { EmitApplication (); })) return ;
36753765 if (!env ()->can_call_into_js ()) return ;
36763766
36773767 if (!has_application ()) {
@@ -3742,6 +3832,10 @@ void Session::EmitNewToken(const uint8_t* token, size_t len) {
37423832void Session::EmitStream (const BaseObjectWeakPtr<Stream>& stream) {
37433833 DCHECK (!is_destroyed ());
37443834
3835+ if (DeferEmit ([this , stream] { EmitStream (stream); })) return ;
3836+
3837+ if (!stream) return ;
3838+
37453839 if (!env ()->can_call_into_js ()) return ;
37463840 CallbackScope<Session> cb_scope (this );
37473841
@@ -3797,6 +3891,14 @@ void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd,
37973891
37983892void Session::EmitOrigins (std::vector<std::string>&& origins) {
37993893 DCHECK (!is_destroyed ());
3894+
3895+ if (must_defer_emits ()) {
3896+ QueueDeferredEmit ([this , origins = std::move (origins)]() mutable {
3897+ EmitOrigins (std::move (origins));
3898+ });
3899+ return ;
3900+ }
3901+
38003902 if (!HasListenerFlag (impl_->state ()->listener_flags ,
38013903 SessionListenerFlags::ORIGIN ))
38023904 return ;
@@ -3822,11 +3924,17 @@ void Session::EmitOrigins(std::vector<std::string>&& origins) {
38223924
38233925void Session::EmitKeylog (const char * line) {
38243926 DCHECK (!is_destroyed ());
3927+
3928+ if (must_defer_emits ()) {
3929+ QueueDeferredEmit (
3930+ [this , str = std::string (line)]() { EmitKeylog (str.c_str ()); });
3931+ return ;
3932+ }
3933+
38253934 if (!env ()->can_call_into_js ()) return ;
38263935
3827- auto str = std::string (line);
38283936 Local<Value> argv[] = {Undefined (env ()->isolate ())};
3829- if (!ToV8Value (env ()->context (), str ).ToLocal (&argv[0 ])) {
3937+ if (!ToV8Value (env ()->context (), std::string (line) ).ToLocal (&argv[0 ])) {
38303938 Debug (this , " Failed to convert keylog line to V8 string" );
38313939 return ;
38323940 }
0 commit comments