| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent e493f04 commit 7652bd9
18 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1659,6 +1659,11 @@ added: v23.8.0 | |||
| 1659 | 1659 | ||
| 1660 | 1660 | * Type: {bigint} | |
| 1661 | 1661 | ||
| 1662 | + ### `sessionStats.streamsIdleTimedOut` | ||
| 1663 | + | ||
| 1664 | + * Type: {bigint} The total number of peer-initiated streams destroyed by the | ||
| 1665 | + stream idle timeout. Read only. | ||
| 1666 | + | ||
| 1662 | 1667 | ## Class: `QuicError` | |
| 1663 | 1668 | ||
| 1664 | 1669 | <!-- YAML | |
@@ -3026,6 +3031,23 @@ reported as lost via the `ondatagramstatus` callback. | |||
| 3026 | 3031 | ||
| 3027 | 3032 | This option is immutable after session creation. | |
| 3028 | 3033 | ||
| 3034 | + #### `sessionOptions.streamIdleTimeout` | ||
| 3035 | + | ||
| 3036 | + * Type: {bigint|number} | ||
| 3037 | + * **Default:** `30000` (30 seconds) | ||
| 3038 | + | ||
| 3039 | + The maximum time in milliseconds that a peer-initiated stream can be idle | ||
| 3040 | + (no data received) before it is automatically destroyed. This protects | ||
| 3041 | + against slowloris-style attacks where a remote peer opens streams but never | ||
| 3042 | + sends data, holding server resources indefinitely. Only peer-initiated | ||
| 3043 | + streams are checked — locally-initiated streams are the application's | ||
| 3044 | + responsibility. Set to `0` to disable. | ||
| 3045 | + | ||
| 3046 | + The idle check runs as part of the normal send processing loop, so it adds | ||
| 3047 | + no additional timers or event loop overhead. The | ||
| 3048 | + `session.stats.streamsIdleTimedOut` counter tracks how many streams have been | ||
| 3049 | + destroyed by this mechanism. | ||
| 3050 | + | ||
| 3029 | 3051 | #### `sessionOptions.maxDatagramSendAttempts` | |
| 3030 | 3052 | ||
| 3031 | 3053 | * Type: {number} | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -436,6 +436,7 @@ const endpointRegistry = new SafeSet(); | |||
| 436 | 436 | * @property {number} [drainingPeriodMultiplier] Multiplier applied to the | |
| 437 | 437 | * draining period (3 * PTO) used by ngtcp2. Range `3..255`. | |
| 438 | 438 | * **Default:** `3`. | |
| 439 | + * @property {bigint|number} [streamIdleTimeout] Time in ms before idle peer-initiated streams are destroyed | ||
| 439 | 440 | * @property {number} [maxDatagramSendAttempts] Maximum number of times a | |
| 440 | 441 | * datagram is retried before being abandoned. Range `1..255`. | |
| 441 | 442 | * **Default:** `5`. | |
@@ -922,6 +923,17 @@ setCallbacks({ | |||
| 922 | 923 | // from QuicError::ToV8Value. Convert to a proper Node.js Error. | |
| 923 | 924 | if (error !== undefined) { | |
| 924 | 925 | error = convertQuicError(error); | |
| 926 | + } else if (this[kOwner] && !this[kOwner].destroyed) { | ||
| 927 | + // The stream is closing cleanly, but it may have been reset by the | ||
| 928 | + // peer (ReceiveStreamReset) or locally (resetStream). The C++ side | ||
| 929 | + // records the reset code in state.resetCode. If set, surface the | ||
| 930 | + // reset as the close error so stream.closed rejects -- the reset | ||
| 931 | + // was an abnormal termination even if the session closed cleanly. | ||
| 932 | + const resetCode = getQuicStreamState(this[kOwner]).resetCode; | ||
| 933 | + if (resetCode !== undefined && resetCode > 0n) { | ||
| 934 | + error = new ERR_QUIC_APPLICATION_ERROR( | ||
| 935 | + resetCode, `stream reset with code ${resetCode}`); | ||
| 936 | + } | ||
| 925 | 937 | } | |
| 926 | 938 | debug(`stream ${this[kOwner].id} closed callback with error: ${error}`); | |
| 927 | 939 | this[kOwner][kFinishClose](error); | |
@@ -5015,6 +5027,7 @@ function processSessionOptions(options, config = kEmptyObject) { | |||
| 5015 | 5027 | datagramDropPolicy = 'drop-oldest', | |
| 5016 | 5028 | drainingPeriodMultiplier = 3, | |
| 5017 | 5029 | maxDatagramSendAttempts = 5, | |
| 5030 | + streamIdleTimeout, | ||
| 5018 | 5031 | verifyPeer = 'auto', | |
| 5019 | 5032 | // HTTP/3 application-specific options. Nested under `application` | |
| 5020 | 5033 | // to separate protocol-specific settings from transport-level ones. | |
@@ -5136,6 +5149,7 @@ function processSessionOptions(options, config = kEmptyObject) { | |||
| 5136 | 5149 | datagramDropPolicy, | |
| 5137 | 5150 | drainingPeriodMultiplier, | |
| 5138 | 5151 | maxDatagramSendAttempts, | |
| 5152 | + streamIdleTimeout, | ||
| 5139 | 5153 | application, | |
| 5140 | 5154 | onerror, | |
| 5141 | 5155 | onstream, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -101,6 +101,7 @@ const { | |||
| 101 | 101 | IDX_STATS_SESSION_DATAGRAMS_SENT, | |
| 102 | 102 | IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED, | |
| 103 | 103 | IDX_STATS_SESSION_DATAGRAMS_LOST, | |
| 104 | + IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT, | ||
| 104 | 105 | IDX_STATS_SESSION_COUNT, | |
| 105 | 106 | ||
| 106 | 107 | IDX_STATS_STREAM_CREATED_AT, | |
@@ -169,6 +170,7 @@ assert(IDX_STATS_SESSION_DATAGRAMS_RECEIVED !== undefined); | |||
| 169 | 170 | assert(IDX_STATS_SESSION_DATAGRAMS_SENT !== undefined); | |
| 170 | 171 | assert(IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED !== undefined); | |
| 171 | 172 | assert(IDX_STATS_SESSION_DATAGRAMS_LOST !== undefined); | |
| 173 | + assert(IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT !== undefined); | ||
| 172 | 174 | assert(IDX_STATS_STREAM_CREATED_AT !== undefined); | |
| 173 | 175 | assert(IDX_STATS_STREAM_OPENED_AT !== undefined); | |
| 174 | 176 | assert(IDX_STATS_STREAM_RECEIVED_AT !== undefined); | |
@@ -689,6 +691,13 @@ class QuicSessionStats { | |||
| 689 | 691 | return this.#handle[this.#offset + IDX_STATS_SESSION_DATAGRAMS_LOST]; | |
| 690 | 692 | } | |
| 691 | 693 | ||
| 694 | + /** @type {bigint} */ | ||
| 695 | + get streamsIdleTimedOut() { | ||
| 696 | + assertIsQuicSessionStats(this); | ||
| 697 | + return this.#handle[this.#offset + | ||
| 698 | + IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT]; | ||
| 699 | + } | ||
| 700 | + | ||
| 692 | 701 | toString() { | |
| 693 | 702 | return JSONStringify(this.toJSON()); | |
| 694 | 703 | } | |
@@ -726,6 +735,7 @@ class QuicSessionStats { | |||
| 726 | 735 | datagramsSent, | |
| 727 | 736 | datagramsAcknowledged, | |
| 728 | 737 | datagramsLost, | |
| 738 | + streamsIdleTimedOut, | ||
| 729 | 739 | } = this; | |
| 730 | 740 | return { | |
| 731 | 741 | __proto__: null, | |
@@ -762,6 +772,7 @@ class QuicSessionStats { | |||
| 762 | 772 | datagramsSent: `${datagramsSent}`, | |
| 763 | 773 | datagramsAcknowledged: `${datagramsAcknowledged}`, | |
| 764 | 774 | datagramsLost: `${datagramsLost}`, | |
| 775 | + streamsIdleTimedOut: `${streamsIdleTimedOut}`, | ||
| 765 | 776 | }; | |
| 766 | 777 | } | |
| 767 | 778 | ||
@@ -807,6 +818,7 @@ class QuicSessionStats { | |||
| 807 | 818 | datagramsSent, | |
| 808 | 819 | datagramsAcknowledged, | |
| 809 | 820 | datagramsLost, | |
| 821 | + streamsIdleTimedOut, | ||
| 810 | 822 | } = this; | |
| 811 | 823 | ||
| 812 | 824 | return `QuicSessionStats ${inspect({ | |
@@ -841,6 +853,7 @@ class QuicSessionStats { | |||
| 841 | 853 | datagramsSent, | |
| 842 | 854 | datagramsAcknowledged, | |
| 843 | 855 | datagramsLost, | |
| 856 | + streamsIdleTimedOut, | ||
| 844 | 857 | }, opts)}`; | |
| 845 | 858 | } | |
| 846 | 859 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -724,8 +724,11 @@ class DefaultApplication final : public Session::Application { | |||
| 724 | 724 | ||
| 725 | 725 | void EarlyDataRejected() override { | |
| 726 | 726 | // Destroy all open streams — ngtcp2 has already discarded their | |
| 727 | - // internal state when it rejected the early data. | ||
| 728 | - session().DestroyAllStreams(QuicError::ForApplication(0)); | ||
| 727 | + // internal state when it rejected the early data. Use the | ||
| 728 | + // application's internal error code since this is an error | ||
| 729 | + // condition (code 0 would be treated as a clean close). | ||
| 730 | + session().DestroyAllStreams( | ||
| 731 | + QuicError::ForApplication(GetInternalErrorCode())); | ||
| 729 | 732 | if (!session().is_destroyed()) { | |
| 730 | 733 | session().EmitEarlyDataRejected(); | |
| 731 | 734 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -113,6 +113,7 @@ class SessionManager; | |||
| 113 | 113 | V(max_connections_total, "maxConnectionsTotal") \ | |
| 114 | 114 | V(max_datagram_frame_size, "maxDatagramFrameSize") \ | |
| 115 | 115 | V(max_datagram_send_attempts, "maxDatagramSendAttempts") \ | |
| 116 | + V(stream_idle_timeout, "streamIdleTimeout") \ | ||
| 116 | 117 | V(max_field_section_size, "maxFieldSectionSize") \ | |
| 117 | 118 | V(max_header_length, "maxHeaderLength") \ | |
| 118 | 119 | V(max_header_pairs, "maxHeaderPairs") \ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -365,12 +365,12 @@ std::optional<int> QuicError::get_crypto_error() const { | |||
| 365 | 365 | ||
| 366 | 366 | MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const { | |
| 367 | 367 | if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) || | |
| 368 | - (type() == Type::APPLICATION && code() == NGHTTP3_H3_NO_ERROR) || | ||
| 368 | + (type() == Type::APPLICATION && | ||
| 369 | + (code() == 0 || code() == NGHTTP3_H3_NO_ERROR)) || | ||
| 369 | 370 | type() == Type::IDLE_CLOSE) { | |
| 370 | - // Note that we only return undefined for *known* no-error application | ||
| 371 | - // codes. It is possible that other application types use other specific | ||
| 372 | - // no-error codes, but since we don't know which application is being used, | ||
| 373 | - // we'll just return the error code value for those below. | ||
| 371 | + // Application code 0 is the default no-error code for raw QUIC | ||
| 372 | + // applications (DefaultApplication::GetNoErrorCode() returns 0). | ||
| 373 | + // NGHTTP3_H3_NO_ERROR (0x100) is the HTTP/3 no-error code. | ||
| 374 | 374 | // Idle close is always clean — the session timed out normally. | |
| 375 | 375 | return Undefined(env->isolate()); | |
| 376 | 376 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -177,10 +177,13 @@ class Http3ApplicationImpl final : public Session::Application { | |||
| 177 | 177 | // When 0-RTT is rejected, destroy the nghttp3 connection and all | |
| 178 | 178 | // open streams — ngtcp2 has discarded their internal state. | |
| 179 | 179 | // Reset started_ so Start() is called again via on_receive_rx_key | |
| 180 | - // at 1RTT to recreate the nghttp3 connection. | ||
| 180 | + // at 1RTT to recreate the nghttp3 connection. Use the | ||
| 181 | + // application's internal error code since this is an error | ||
| 182 | + // condition (code 0 would be treated as a clean close). | ||
| 181 | 183 | conn_.reset(); | |
| 182 | 184 | started_ = false; | |
| 183 | - session().DestroyAllStreams(QuicError::ForApplication(0)); | ||
| 185 | + session().DestroyAllStreams( | ||
| 186 | + QuicError::ForApplication(GetInternalErrorCode())); | ||
| 184 | 187 | if (!session().is_destroyed()) { | |
| 185 | 188 | session().EmitEarlyDataRejected(); | |
| 186 | 189 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -174,7 +174,8 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) { | |||
| 174 | 174 | V(DATAGRAMS_RECEIVED, datagrams_received) \ | |
| 175 | 175 | V(DATAGRAMS_SENT, datagrams_sent) \ | |
| 176 | 176 | 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) | ||
| 178 | 179 | ||
| 179 | 180 | #define NO_SIDE_EFFECT true | |
| 180 | 181 | #define SIDE_EFFECT false | |
@@ -617,7 +618,8 @@ Maybe<Session::Options> Session::Options::From(Environment* env, | |||
| 617 | 618 | !SET(keep_alive_timeout) || !SET(max_stream_window) || !SET(max_window) || | |
| 618 | 619 | !SET(max_payload_size) || !SET(unacknowledged_packet_threshold) || | |
| 619 | 620 | !SET(cc_algorithm) || !SET(draining_period_multiplier) || | |
| 620 | - !SET(max_datagram_send_attempts)) { | ||
| 621 | + !SET(max_datagram_send_attempts) || | ||
| 622 | + !SET(stream_idle_timeout)) { | ||
| 621 | 623 | return Nothing<Options>(); | |
| 622 | 624 | } | |
| 623 | 625 | ||
@@ -2819,24 +2821,36 @@ void Session::ShutdownStream(stream_id id, QuicError error) { | |||
| 2819 | 2821 | DCHECK(!is_destroyed()); | |
| 2820 | 2822 | Debug(this, "Shutting down stream %" PRIi64 " with error %s", id, error); | |
| 2821 | 2823 | 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); | ||
| 2828 | 2838 | } | |
| 2829 | 2839 | ||
| 2830 | - void Session::ShutdownStreamWrite(stream_id id, QuicError code) { | ||
| 2840 | + void Session::ShutdownStreamWrite(stream_id id, QuicError error) { | ||
| 2831 | 2841 | 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); | ||
| 2833 | 2844 | 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); | ||
| 2840 | 2854 | } | |
| 2841 | 2855 | ||
| 2842 | 2856 | void Session::StreamDataBlocked(stream_id id) { | |
@@ -3035,6 +3049,41 @@ void Session::UpdateDataStats() { | |||
| 3035 | 3049 | std::max(STAT_GET(Stats, max_bytes_in_flight), info.bytes_in_flight)); | |
| 3036 | 3050 | } | |
| 3037 | 3051 | ||
| 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 | + | ||
| 3038 | 3087 | void Session::SendConnectionClose() { | |
| 3039 | 3088 | // Method is a non-op if the session is already destroyed or the | |
| 3040 | 3089 | // endpoint cannot send. Note: we intentionally do NOT check | |
@@ -3119,6 +3168,8 @@ void Session::OnTimeout() { | |||
| 3119 | 3168 | if (is_destroyed()) return; | |
| 3120 | 3169 | if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) { | |
| 3121 | 3170 | application().SendPendingData(); | |
| 3171 | + if (is_destroyed()) return; | ||
| 3172 | + CheckStreamIdleTimeout(uv_hrtime()); | ||
| 3122 | 3173 | return; | |
| 3123 | 3174 | } | |
| 3124 | 3175 | if (is_destroyed()) return; | |
@@ -3165,6 +3216,15 @@ void Session::UpdateTimer() { | |||
| 3165 | 3216 | auto timeout = (expiry - now) / NGTCP2_MILLISECONDS; | |
| 3166 | 3217 | Debug(this, "Updating timeout to %zu milliseconds", timeout); | |
| 3167 | 3218 | ||
| 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 | + | ||
| 3168 | 3228 | // If timeout is zero here, it means our timer is less than a millisecond | |
| 3169 | 3229 | // off from expiry. Let's bump the timer to 1. | |
| 3170 | 3230 | impl_->timer_.Update(timeout == 0 ? 1 : timeout); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -227,6 +227,14 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { | |||
| 227 | 227 | // 10.2 requires at least 3x PTO. Range: 3-255. Default: 3. | |
| 228 | 228 | uint8_t draining_period_multiplier = 3; | |
| 229 | 229 | ||
| 230 | + // The amount of time (in milliseconds) that a stream can be idle | ||
| 231 | + // (no data received) before it is automatically destroyed. This | ||
| 232 | + // protects against slowloris-style attacks where a peer opens streams | ||
| 233 | + // but never sends data, holding server resources indefinitely. | ||
| 234 | + // Only applies to peer-initiated streams. Set to 0 to disable. | ||
| 235 | + static constexpr uint64_t DEFAULT_STREAM_IDLE_TIMEOUT = 30'000; | ||
| 236 | + uint64_t stream_idle_timeout = DEFAULT_STREAM_IDLE_TIMEOUT; | ||
| 237 | + | ||
| 230 | 238 | // An optional NEW_TOKEN from a previous connection to the same | |
| 231 | 239 | // server. When set, the token is included in the Initial packet | |
| 232 | 240 | // to skip address validation. Client-side only. | |
@@ -569,6 +577,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { | |||
| 569 | 577 | // Has to be called after certain operations that generate packets. | |
| 570 | 578 | void UpdatePacketTxTime(); | |
| 571 | 579 | void UpdateDataStats(); | |
| 580 | + void CheckStreamIdleTimeout(uint64_t now); | ||
| 572 | 581 | void UpdatePath(const PathStorage& path); | |
| 573 | 582 | ||
| 574 | 583 | void ProcessPendingBidiStreams(); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1270,7 +1270,8 @@ void Stream::NotifyStreamOpened(stream_id id) { | |||
| 1270 | 1270 | // Headers were enqueued while the application was not yet known | |
| 1271 | 1271 | // (headers_supported == 0), and the negotiated application does | |
| 1272 | 1272 | // not support headers. This is a fatal mismatch. | |
| 1273 | - Destroy(QuicError::ForApplication(0)); | ||
| 1273 | + Destroy(QuicError::ForApplication( | ||
| 1274 | + session().application().GetInternalErrorCode())); | ||
| 1274 | 1275 | return; | |
| 1275 | 1276 | } | |
| 1276 | 1277 | decltype(pending_headers_queue_) queue; | |
@@ -1347,6 +1348,11 @@ Session& Stream::session() const { | |||
| 1347 | 1348 | return *session_; | |
| 1348 | 1349 | } | |
| 1349 | 1350 | ||
| 1351 | + uint64_t Stream::last_activity_timestamp() const { | ||
| 1352 | + uint64_t ts = stats()->received_at; | ||
| 1353 | + return ts != 0 ? ts : stats()->created_at; | ||
| 1354 | + } | ||
| 1355 | + | ||
| 1350 | 1356 | bool Stream::is_local_unidirectional() const { | |
| 1351 | 1357 | return direction() == Direction::UNIDIRECTIONAL && | |
| 1352 | 1358 | ngtcp2_conn_is_local_stream(*session_, id()); | |
@@ -1625,6 +1631,7 @@ void Stream::EndReadable(std::optional<uint64_t> maybe_final_size) { | |||
| 1625 | 1631 | ||
| 1626 | 1632 | void Stream::Destroy(QuicError error) { | |
| 1627 | 1633 | if (stats()->destroyed_at != 0) return; | |
| 1634 | + | ||
| 1628 | 1635 | // Record the destroyed at timestamp before notifying the JavaScript side | |
| 1629 | 1636 | // that the stream is being destroyed. | |
| 1630 | 1637 | STAT_RECORD_TIMESTAMP(Stats, destroyed_at); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments