@@ -174,7 +174,8 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) {
174174 V (DATAGRAMS_RECEIVED , datagrams_received) \
175175 V (DATAGRAMS_SENT , datagrams_sent) \
176176 V (DATAGRAMS_ACKNOWLEDGED , datagrams_acknowledged) \
177- V (DATAGRAMS_LOST , datagrams_lost)
177+ V (DATAGRAMS_LOST , datagrams_lost) \
178+ V (STREAMS_IDLE_TIMED_OUT , streams_idle_timed_out)
178179
179180#define NO_SIDE_EFFECT true
180181#define SIDE_EFFECT false
@@ -617,7 +618,8 @@ Maybe<Session::Options> Session::Options::From(Environment* env,
617618 !SET (keep_alive_timeout) || !SET (max_stream_window) || !SET (max_window) ||
618619 !SET (max_payload_size) || !SET (unacknowledged_packet_threshold) ||
619620 !SET (cc_algorithm) || !SET (draining_period_multiplier) ||
620- !SET (max_datagram_send_attempts)) {
621+ !SET (max_datagram_send_attempts) ||
622+ !SET (stream_idle_timeout)) {
621623 return Nothing<Options>();
622624 }
623625
@@ -2819,24 +2821,36 @@ void Session::ShutdownStream(stream_id id, QuicError error) {
28192821 DCHECK (!is_destroyed ());
28202822 Debug (this , " Shutting down stream %" PRIi64 " with error %s" , id, error);
28212823 SendPendingDataScope send_scope (this );
2822- ngtcp2_conn_shutdown_stream (*this ,
2823- 0 ,
2824- id,
2825- error.type () == QuicError::Type::APPLICATION
2826- ? error.code ()
2827- : application ().GetNoErrorCode ());
2824+ // STOP_SENDING and RESET_STREAM frames carry application-level error
2825+ // codes (RFC 9000 §19.4, §19.5). Map the QuicError to an appropriate
2826+ // application code: APPLICATION errors pass through directly; transport
2827+ // no-error maps to the application's no-error code; any other error
2828+ // maps to the application's internal error code.
2829+ error_code code;
2830+ if (error.type () == QuicError::Type::APPLICATION ) {
2831+ code = error.code ();
2832+ } else if (error.code () == NGTCP2_NO_ERROR ) {
2833+ code = application ().GetNoErrorCode ();
2834+ } else {
2835+ code = application ().GetInternalErrorCode ();
2836+ }
2837+ ngtcp2_conn_shutdown_stream (*this , 0 , id, code);
28282838}
28292839
2830- void Session::ShutdownStreamWrite (stream_id id, QuicError code ) {
2840+ void Session::ShutdownStreamWrite (stream_id id, QuicError error ) {
28312841 DCHECK (!is_destroyed ());
2832- Debug (this , " Shutting down stream %" PRIi64 " write with error %s" , id, code);
2842+ Debug (this , " Shutting down stream %" PRIi64 " write with error %s" ,
2843+ id, error);
28332844 SendPendingDataScope send_scope (this );
2834- ngtcp2_conn_shutdown_stream_write (*this ,
2835- 0 ,
2836- id,
2837- code.type () == QuicError::Type::APPLICATION
2838- ? code.code ()
2839- : application ().GetNoErrorCode ());
2845+ error_code code;
2846+ if (error.type () == QuicError::Type::APPLICATION ) {
2847+ code = error.code ();
2848+ } else if (error.code () == NGTCP2_NO_ERROR ) {
2849+ code = application ().GetNoErrorCode ();
2850+ } else {
2851+ code = application ().GetInternalErrorCode ();
2852+ }
2853+ ngtcp2_conn_shutdown_stream_write (*this , 0 , id, code);
28402854}
28412855
28422856void Session::StreamDataBlocked (stream_id id) {
@@ -3035,6 +3049,41 @@ void Session::UpdateDataStats() {
30353049 std::max (STAT_GET (Stats, max_bytes_in_flight), info.bytes_in_flight ));
30363050}
30373051
3052+ void Session::CheckStreamIdleTimeout (uint64_t now) {
3053+ if (is_destroyed ()) return ;
3054+ uint64_t timeout = options ().stream_idle_timeout ;
3055+ if (timeout == 0 ) return ;
3056+
3057+ uint64_t timeout_ns = timeout * NGTCP2_MILLISECONDS ;
3058+ auto all_streams = streams ();
3059+
3060+ for (const auto & [id, stream] : all_streams) {
3061+ if (!stream) continue ;
3062+
3063+ // Only check peer-initiated streams. Locally-initiated streams
3064+ // that haven't been written to are the application's concern.
3065+ if (ngtcp2_conn_is_local_stream (*this , id)) continue ;
3066+
3067+ uint64_t last_activity = stream->last_activity_timestamp ();
3068+ if (last_activity > 0 && (now - last_activity) > timeout_ns) {
3069+ Debug (this ,
3070+ " Stream %" PRId64 " idle timeout exceeded, destroying" ,
3071+ id);
3072+ // Notify the peer before destroying. ShutdownStream sends both
3073+ // STOP_SENDING and RESET_STREAM as appropriate, using the
3074+ // application's no-error code for non-APPLICATION errors (since
3075+ // these frames carry application-level error codes per RFC 9000).
3076+ // Without this, the peer's stream sits orphaned until the
3077+ // session closes.
3078+ auto error = QuicError::ForTransport (NGTCP2_ERR_PROTO ,
3079+ " stream idle timeout" );
3080+ ShutdownStream (id, error);
3081+ stream->Destroy (error);
3082+ STAT_INCREMENT (Stats, streams_idle_timed_out);
3083+ }
3084+ }
3085+ }
3086+
30383087void Session::SendConnectionClose () {
30393088 // Method is a non-op if the session is already destroyed or the
30403089 // endpoint cannot send. Note: we intentionally do NOT check
@@ -3119,6 +3168,8 @@ void Session::OnTimeout() {
31193168 if (is_destroyed ()) return ;
31203169 if (NGTCP2_OK (ret) && !is_in_closing_period () && !is_in_draining_period ()) {
31213170 application ().SendPendingData ();
3171+ if (is_destroyed ()) return ;
3172+ CheckStreamIdleTimeout (uv_hrtime ());
31223173 return ;
31233174 }
31243175 if (is_destroyed ()) return ;
@@ -3165,6 +3216,15 @@ void Session::UpdateTimer() {
31653216 auto timeout = (expiry - now) / NGTCP2_MILLISECONDS ;
31663217 Debug (this , " Updating timeout to %zu milliseconds" , timeout);
31673218
3219+ // If a stream idle timeout is configured, ensure the timer fires at
3220+ // least that often so CheckStreamIdleTimeout runs. Without this, an
3221+ // idle session with idle streams might not fire the timer until the
3222+ // connection idle timeout, which could be much longer.
3223+ uint64_t stream_idle = options ().stream_idle_timeout ;
3224+ if (stream_idle > 0 && timeout > stream_idle) {
3225+ timeout = stream_idle;
3226+ }
3227+
31683228 // If timeout is zero here, it means our timer is less than a millisecond
31693229 // off from expiry. Let's bump the timer to 1.
31703230 impl_->timer_ .Update (timeout == 0 ? 1 : timeout);
0 commit comments