Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/api/Client.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Returns: `Client`
* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
* **initialWindowSize**: `number` (optional) - Default: `262144` (256KB). Sets the HTTP/2 stream-level flow-control window size (SETTINGS_INITIAL_WINDOW_SIZE). Must be a positive integer greater than 0. This default is higher than Node.js core's default (65535 bytes) to improve throughput, Node's choice is very conservative for current high-bandwith networks. See [RFC 7540 Section 6.9.2](https://datatracker.ietf.org/doc/html/rfc7540#section-6.9.2) for more details.
* **connectionWindowSize**: `number` (optional) - Default `524288` (512KB). Sets the HTTP/2 connection-level flow-control window size using `ClientHttp2Session.setLocalWindowSize()`. Must be a positive integer greater than 0. This provides better flow control for the entire connection across multiple streams. See [Node.js HTTP/2 documentation](https://nodejs.org/api/http2.html#clienthttp2sessionsetlocalwindowsize) for more details.
* **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections. This will emit a `ping` event on the client with the duration of the ping in milliseconds.

> **Notes about HTTP/2**
> - It only works under TLS connections. h2c is not supported.
Expand Down
1 change: 1 addition & 0 deletions docs/docs/api/H2CClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Returns: `H2CClient`
- **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
- **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
- **pipelining** `number | null` (optional) - Default to `maxConcurrentStreams` - The amount of concurrent requests sent over a single HTTP/2 session in accordance with [RFC-7540](https://httpwg.org/specs/rfc7540.html#StreamsLayer) Stream specification. Streams can be closed up by remote server at any time.
- **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections.
- **connect** `ConnectOptions | null` (optional) - Default: `null`.
- **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
- **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
Expand Down
1 change: 1 addition & 0 deletions lib/core/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ module.exports = {
kEnableConnectProtocol: Symbol('http2session connect protocol'),
kRemoteSettings: Symbol('http2session remote settings'),
kHTTP2Stream: Symbol('http2session client stream'),
kPingInterval: Symbol('ping interval'),
kNoProxyAgent: Symbol('no proxy agent'),
kHttpProxyAgent: Symbol('http proxy agent'),
kHttpsProxyAgent: Symbol('https proxy agent')
Expand Down
43 changes: 40 additions & 3 deletions lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const {
kStrictContentLength,
kOnError,
kMaxConcurrentStreams,
kPingInterval,
kHTTP2Session,
kHTTP2InitialWindowSize,
kHTTP2ConnectionWindowSize,
Expand All @@ -34,7 +35,8 @@ const {
kBodyTimeout,
kEnableConnectProtocol,
kRemoteSettings,
kHTTP2Stream
kHTTP2Stream,
kHTTP2SessionState
} = require('../core/symbols.js')
const { channels } = require('../core/diagnostics.js')

Expand Down Expand Up @@ -102,10 +104,15 @@ function connectH2 (client, socket) {
}
})

client[kSocket] = socket
session[kOpenStreams] = 0
session[kClient] = client
session[kSocket] = socket
session[kHTTP2Session] = null
session[kHTTP2SessionState] = {
ping: {
interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
}
}
// We set it to true by default in a best-effort; however once connected to an H2 server
// we will check if extended CONNECT protocol is supported or not
// and set this value accordingly.
Expand Down Expand Up @@ -253,6 +260,31 @@ function onHttp2RemoteSettings (settings) {
this[kClient][kResume]()
}

function onHttp2SendPing (session) {
const state = session[kHTTP2SessionState]
if ((session.closed || session.destroyed) && state.ping.interval != null) {
clearInterval(state.ping.interval)
state.ping.interval = null
return
}

// If no ping sent, do nothing
session.ping(onPing.bind(session))

function onPing (err, duration) {
const client = this[kClient]
const socket = this[kClient]

if (err != null) {
const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`)
socket[kError] = error
client[kOnError](error)
} else {
client.emit('ping', duration)
}
}
}

function onHttp2SessionError (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')

Expand Down Expand Up @@ -316,14 +348,19 @@ function onHttp2SessionGoAway (errorCode) {
}

function onHttp2SessionClose () {
const { [kClient]: client } = this
const { [kClient]: client, [kHTTP2SessionState]: state } = this
const { [kSocket]: socket } = client

const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))

client[kSocket] = null
client[kHTTPContext] = null

if (state.ping.interval != null) {
clearInterval(state.ping.interval)
state.ping.interval = null
}

if (client.destroyed) {
assert(client[kPending] === 0)

Expand Down
14 changes: 11 additions & 3 deletions lib/dispatcher/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ const {
kMaxConcurrentStreams,
kHTTP2InitialWindowSize,
kHTTP2ConnectionWindowSize,
kResume
kResume,
kPingInterval
} = require('../core/symbols.js')
const connectH1 = require('./client-h1.js')
const connectH2 = require('./client-h2.js')
Expand Down Expand Up @@ -112,7 +113,8 @@ class Client extends DispatcherBase {
allowH2,
useH2c,
initialWindowSize,
connectionWindowSize
connectionWindowSize,
pingInterval
} = {}) {
if (keepAlive !== undefined) {
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
Expand Down Expand Up @@ -216,6 +218,10 @@ class Client extends DispatcherBase {
throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0')
}

if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) {
throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
}

super()

if (typeof connect !== 'function') {
Expand Down Expand Up @@ -250,6 +256,8 @@ class Client extends DispatcherBase {
this[kMaxRequests] = maxRequestsPerClient
this[kClosedResolve] = null
this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
this[kHTTPContext] = null
// h2
this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server
// HTTP/2 window sizes are set to higher defaults than Node.js core for better performance:
// - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1)
Expand All @@ -259,7 +267,7 @@ class Client extends DispatcherBase {
// Provides better flow control for the entire connection across multiple streams.
this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144
this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288
this[kHTTPContext] = null
this[kPingInterval] = pingInterval != null ? pingInterval : 60e3 // Default ping interval for h2 - 1 minute

// kQueue is built up of 3 sections separated by
// the kRunningIdx and kPendingIdx indices.
Expand Down
Loading
Loading