Skip to content
Open
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 @@ -31,6 +31,7 @@ Returns: `Client`
* **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
* **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
* **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.
* **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.
- **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 @@ -62,6 +62,7 @@ module.exports = {
kListeners: Symbol('listeners'),
kHTTPContext: Symbol('http context'),
kMaxConcurrentStreams: Symbol('max concurrent streams'),
kPingInterval: Symbol('ping interval'),
kNoProxyAgent: Symbol('no proxy agent'),
kHttpProxyAgent: Symbol('http proxy agent'),
kHttpsProxyAgent: Symbol('https proxy agent')
Expand Down
45 changes: 40 additions & 5 deletions lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ const {
kStrictContentLength,
kOnError,
kMaxConcurrentStreams,
kPingInterval,
kHTTP2Session,
kResume,
kSize,
kHTTPContext,
kClosed,
kBodyTimeout
kBodyTimeout,
kHTTP2SessionState
} = require('../core/symbols.js')
const { channels } = require('../core/diagnostics.js')

Expand Down Expand Up @@ -78,8 +80,6 @@ function parseH2Headers (headers) {
}

async function connectH2 (client, socket) {
client[kSocket] = socket

const session = http2.connect(client[kUrl], {
createConnection: () => socket,
peerMaxConcurrentStreams: client[kMaxConcurrentStreams],
Expand All @@ -89,10 +89,15 @@ async 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()
}
}

util.addListener(session, 'error', onHttp2SessionError)
util.addListener(session, 'frameError', onHttp2FrameError)
Expand Down Expand Up @@ -151,6 +156,31 @@ function resumeH2 (client) {
}
}

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 @@ -214,14 +244,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 @@ -52,7 +52,8 @@ const {
kOnError,
kHTTPContext,
kMaxConcurrentStreams,
kResume
kResume,
kPingInterval
} = require('../core/symbols.js')
const connectH1 = require('./client-h1.js')
const connectH2 = require('./client-h2.js')
Expand Down Expand Up @@ -107,7 +108,8 @@ class Client extends DispatcherBase {
autoSelectFamilyAttemptTimeout,
// h2
maxConcurrentStreams,
allowH2
allowH2,
pingInterval
} = {}) {
if (keepAlive !== undefined) {
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
Expand Down Expand Up @@ -199,6 +201,10 @@ class Client extends DispatcherBase {
throw new InvalidArgumentError('maxConcurrentStreams 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 @@ -232,8 +238,10 @@ class Client extends DispatcherBase {
this[kMaxRequests] = maxRequestsPerClient
this[kClosedResolve] = null
this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server
this[kHTTPContext] = null
// h2
this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server
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