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
2 changes: 2 additions & 0 deletions deps/undici/src/docs/docs/api/Client.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Returns: `Client`
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
* **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit.
* **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
* **eventSource** `EventSourceOptions` (optional) - EventSource-specific configuration options.
* **maxEventSize** `number` (optional) - The maximum total bytes of data and metadata field values retained for a pending EventSource event. Each unfinished line is separately limited to this value plus seven syntax bytes. The number of `data:` lines per event and input chunks buffered for an unfinished line are each limited to `min(131072, max(1024, floor(maxEventSize / 128)))` to bound object overhead. Set to `0` to disable these limits. Default: `buffer.kStringMaxLength`.
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
* **connect** `ConnectOptions | Function | 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.
Expand Down
4 changes: 3 additions & 1 deletion deps/undici/src/docs/docs/api/DiagnosticsChannel.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) =>

## `undici:request:headers`

This message is published after the response headers have been received, i.e. the response has been completed.
This message is published after the response headers have been received. This includes a successful CONNECT or
protocol upgrade response.

```js
import diagnosticsChannel from 'diagnostics_channel'
Expand All @@ -57,6 +58,7 @@ diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, respo
## `undici:request:trailers`

This message is published after the response body and trailers have been received, i.e. the response has been completed.
After an upgraded socket is passed to the request handler, this message is published with an empty `trailers` array.

```js
import diagnosticsChannel from 'diagnostics_channel'
Expand Down
70 changes: 68 additions & 2 deletions deps/undici/src/lib/core/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,77 @@ class Request {
}
}

onUpgrade (statusCode, headers, socket) {
/**
* @param {number|null} statusCode
* @param {Buffer[]|null} headers
* @param {import('node:stream').Duplex} socket
* @param {string} [statusText]
*/
onUpgrade (statusCode, headers, socket, statusText = '') {
this.onFinally()

assert(!this.aborted)
assert(!this.completed)

return this[kHandler].onUpgrade(statusCode, headers, socket)
if (statusCode !== null) {
this.#publishUpgradeHeaders(statusCode, headers, statusText)
}

const result = this[kHandler].onUpgrade(statusCode, headers, socket)

if (!this.aborted) {
this.completed = true
if (statusCode !== null) {
this.#publishUpgradeTrailers()
}
}

return result
}

/**
* @param {number} statusCode
* @param {import('node:http2').IncomingHttpHeaders} headers
* @param {(headers: import('node:http2').IncomingHttpHeaders) => Buffer[]} parseHeaders
* @param {string} [statusText]
*/
onUpgradeResponse (statusCode, headers, parseHeaders, statusText = '') {
assert(!this.aborted)
assert(this.completed)

if (channels.headers.hasSubscribers) {
this.#publishUpgradeHeaders(statusCode, parseHeaders(headers), statusText)
}
this.#publishUpgradeTrailers()
}

/**
* @param {Error} error
*/
onUpgradeError (error) {
assert(!this.aborted)
assert(this.completed)

if (channels.error.hasSubscribers) {
channels.error.publish({ request: this, error })
}
}

/**
* @param {number} statusCode
* @param {Buffer[]} headers
* @param {string} statusText
*/
#publishUpgradeHeaders (statusCode, headers, statusText) {
if (channels.headers.hasSubscribers) {
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
}
}

#publishUpgradeTrailers () {
if (channels.trailers.hasSubscribers) {
channels.trailers.publish({ request: this, trailers: [] })
}
}

onComplete (trailers) {
Expand Down
8 changes: 6 additions & 2 deletions deps/undici/src/lib/dispatcher/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function defaultFactory (origin, opts) {
}

class Agent extends DispatcherBase {
constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
constructor ({ factory = defaultFactory, maxRedirections = 0, connect, tls, ...options } = {}) {
if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.')
}
Expand All @@ -42,11 +42,15 @@ class Agent extends DispatcherBase {
connect = { ...connect }
}

if (tls && typeof tls !== 'function') {
tls = { ...tls }
}

this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent)
? options.interceptors.Agent
: [createRedirectInterceptor({ maxRedirections })]

this[kOptions] = { ...util.deepClone(options), connect }
this[kOptions] = { ...util.deepClone(options), connect, tls }
this[kOptions].interceptors = options.interceptors
? { ...options.interceptors }
: undefined
Expand Down
27 changes: 18 additions & 9 deletions deps/undici/src/lib/dispatcher/client-h1.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ class Parser {
}

onUpgrade (head) {
const { upgrade, client, socket, headers, statusCode } = this
const { upgrade, client, socket, headers, statusCode, statusText } = this

assert(upgrade)
assert(client[kSocket] === socket)
Expand Down Expand Up @@ -467,9 +467,10 @@ class Parser {
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))

try {
request.onUpgrade(statusCode, headers, socket)
} catch (err) {
util.destroy(socket, err)
request.onUpgrade(statusCode, headers, socket, statusText)
} catch (error) {
util.errorRequest(client, request, error)
util.destroy(socket, error)
}

client[kResume]()
Expand Down Expand Up @@ -726,8 +727,6 @@ async function connectH1 (client, socket) {
socket[kParser] = new Parser(client, socket, llhttpInstance)

addListener(socket, 'error', function (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')

const parser = this[kParser]

// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
Expand Down Expand Up @@ -1050,12 +1049,22 @@ function writeH1 (client, request) {
const socket = client[kSocket]
clearIdleSocketValidation(socket)

const abort = (err) => {
if (request.aborted || request.completed) {
/**
* @param {Error} [error]
*/
const abort = (error) => {
if (request.aborted) {
return
}

if (request.completed) {
if (request.upgrade || request.method === 'CONNECT') {
util.destroy(socket, new InformationalError('aborted'))
}
return
}

util.errorRequest(client, request, err || new RequestAbortedError())
util.errorRequest(client, request, error || new RequestAbortedError())

util.destroy(body)
util.destroy(socket, new InformationalError('aborted'))
Expand Down
92 changes: 70 additions & 22 deletions deps/undici/src/lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const assert = require('node:assert')
const { errorMonitor } = require('node:events')
const { pipeline } = require('node:stream')
const util = require('../core/util.js')
const {
Expand Down Expand Up @@ -77,6 +78,15 @@ function parseH2Headers (headers) {
return result
}

/**
* @param {import('node:http2').IncomingHttpHeaders} headers
* @returns {Buffer[]}
*/
function parseH2ResponseHeaders (headers) {
const { [HTTP2_HEADER_STATUS]: _statusCode, ...realHeaders } = headers
return parseH2Headers(realHeaders)
}

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

Expand Down Expand Up @@ -126,8 +136,6 @@ async function connectH2 (client, socket) {
socket[kHTTP2Session] = session

util.addListener(socket, 'error', function (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')

this[kError] = err

this[kClient][kOnError](err)
Expand Down Expand Up @@ -201,8 +209,6 @@ function resumeH2 (client) {
}

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

this[kSocket][kError] = err
this[kClient][kOnError](err)
}
Expand Down Expand Up @@ -297,22 +303,32 @@ function writeH2 (client, request) {
headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
headers[HTTP2_HEADER_METHOD] = method

const abort = (err) => {
if (request.aborted || request.completed) {
/**
* @param {Error} [error]
*/
const abort = (error) => {
if (request.aborted) {
return
}

err = err || new RequestAbortedError()
if (request.completed) {
if (method === 'CONNECT' && stream != null) {
util.destroy(stream, error || new RequestAbortedError())
}
return
}

util.errorRequest(client, request, err)
error = error || new RequestAbortedError()

util.errorRequest(client, request, error)

if (stream != null) {
util.destroy(stream, err)
util.destroy(stream, error)
}

// We do not destroy the socket as we can continue using the session
// the stream get's destroyed and the session remains to create new streams
util.destroy(body, err)
util.destroy(body, error)
client[kQueue][client[kRunningIdx]++] = null
client[kResume]()
}
Expand All @@ -331,25 +347,57 @@ function writeH2 (client, request) {

if (method === 'CONNECT') {
session.ref()
// We are already connected, streams are pending, first request
// will create a new stream. We trigger a request to create the stream and wait until
// `ready` event is triggered
// We disabled endStream to allow the user to write to the stream
stream = session.request(headers, { endStream: false, signal })
let upgradeResponseFinished = false

/**
* @param {import('node:http2').IncomingHttpHeaders} headers
*/
const onResponse = (headers) => {
upgradeResponseFinished = true
stream.off(errorMonitor, onUpgradeError)
request.onUpgradeResponse(Number(headers[HTTP2_HEADER_STATUS]), headers, parseH2ResponseHeaders)
}

if (stream.id && !stream.pending) {
request.onUpgrade(null, null, stream)
++session[kOpenStreams]
client[kQueue][client[kRunningIdx]++] = null
} else {
stream.once('ready', () => {
/**
* @param {Error} error
*/
const onUpgradeError = (error) => {
upgradeResponseFinished = true
stream.off('response', onResponse)
request.onUpgradeError(error)
}

const onReady = () => {
try {
request.onUpgrade(null, null, stream)
++session[kOpenStreams]
client[kQueue][client[kRunningIdx]++] = null
})
} catch (error) {
stream.off('response', onResponse)
abort(error)
return
}

if (request.aborted) {
return
}

stream.off('error', abort)
stream.once(errorMonitor, onUpgradeError)
client[kQueue][client[kRunningIdx]++] = null
}

stream.once('response', onResponse)
stream.once('error', abort)
++session[kOpenStreams]
onReady()

stream.once('close', () => {
if (!upgradeResponseFinished && request.completed) {
stream.off('response', onResponse)
stream.off(errorMonitor, onUpgradeError)
request.onUpgradeError(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
}
session[kOpenStreams] -= 1
if (session[kOpenStreams] === 0) session.unref()
})
Expand Down
5 changes: 3 additions & 2 deletions deps/undici/src/lib/dispatcher/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,10 @@ class Client extends DispatcherBase {
// h2
maxConcurrentStreams,
allowH2,
webSocket
webSocket,
eventSource
} = {}) {
super({ webSocket })
super({ webSocket, eventSource })

if (keepAlive !== undefined) {
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
Expand Down
9 changes: 9 additions & 0 deletions deps/undici/src/lib/dispatcher/dispatcher-base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const buffer = require('node:buffer')
const Dispatcher = require('./dispatcher')
const {
ClientDestroyedError,
Expand All @@ -12,6 +13,7 @@ const kOnDestroyed = Symbol('onDestroyed')
const kOnClosed = Symbol('onClosed')
const kInterceptedDispatch = Symbol('Intercepted Dispatch')
const kWebSocketOptions = Symbol('webSocketOptions')
const kEventSourceOptions = Symbol('eventSourceOptions')

class DispatcherBase extends Dispatcher {
constructor (opts) {
Expand All @@ -22,6 +24,7 @@ class DispatcherBase extends Dispatcher {
this[kClosed] = false
this[kOnClosed] = []
this[kWebSocketOptions] = opts?.webSocket ?? {}
this[kEventSourceOptions] = opts?.eventSource ?? {}
}

get webSocketOptions () {
Expand All @@ -31,6 +34,12 @@ class DispatcherBase extends Dispatcher {
}
}

get eventSourceOptions () {
return {
maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength
}
}

get destroyed () {
return this[kDestroyed]
}
Expand Down
Loading
Loading