undici 8.2.0 → 8.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -23
- package/docs/docs/api/Agent.md +3 -0
- package/docs/docs/api/Client.md +43 -5
- package/docs/docs/api/Connector.md +1 -0
- package/docs/docs/api/Dispatcher.md +7 -0
- package/docs/docs/api/Errors.md +12 -0
- package/docs/docs/api/EventSource.md +50 -3
- package/docs/docs/api/Fetch.md +3 -1
- package/docs/docs/api/GlobalInstallation.md +7 -5
- package/docs/docs/api/H2CClient.md +2 -2
- package/docs/docs/api/Pool.md +3 -0
- package/docs/docs/api/RedirectHandler.md +4 -1
- package/docs/docs/api/SnapshotAgent.md +23 -0
- package/lib/api/api-pipeline.js +4 -0
- package/lib/api/api-stream.js +51 -5
- package/lib/core/connect.js +29 -4
- package/lib/core/symbols.js +1 -0
- package/lib/core/util.js +10 -8
- package/lib/dispatcher/client-h1.js +59 -18
- package/lib/dispatcher/client-h2.js +418 -298
- package/lib/dispatcher/client.js +25 -4
- package/lib/dispatcher/pool-base.js +21 -3
- package/lib/dispatcher/pool.js +23 -0
- package/lib/dispatcher/proxy-agent.js +21 -4
- package/lib/dispatcher/round-robin-pool.js +26 -0
- package/lib/dispatcher/socks5-proxy-agent.js +19 -19
- package/lib/handler/redirect-handler.js +36 -11
- package/lib/handler/retry-handler.js +14 -0
- package/lib/interceptor/redirect.js +3 -3
- package/lib/mock/mock-call-history.js +1 -1
- package/lib/mock/mock-utils.js +3 -1
- package/lib/mock/snapshot-agent.js +11 -1
- package/lib/mock/snapshot-recorder.js +38 -3
- package/lib/web/fetch/body.js +2 -7
- package/lib/web/fetch/formdata.js +21 -2
- package/lib/web/fetch/index.js +19 -3
- package/lib/web/fetch/request.js +32 -3
- package/package.json +4 -4
- package/types/client.d.ts +7 -7
- package/types/connector.d.ts +1 -0
- package/types/dispatcher.d.ts +0 -2
- package/types/fetch.d.ts +4 -1
- package/types/formdata.d.ts +0 -6
- package/types/interceptors.d.ts +1 -1
- package/types/snapshot-agent.d.ts +4 -0
package/lib/core/connect.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const net = require('node:net')
|
|
4
4
|
const assert = require('node:assert')
|
|
5
5
|
const util = require('./util')
|
|
6
|
-
const { InvalidArgumentError } = require('./errors')
|
|
6
|
+
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
|
|
7
7
|
|
|
8
8
|
let tls // include tls conditionally since it is not always available
|
|
9
9
|
|
|
@@ -59,7 +59,7 @@ const SessionCache = class WeakSessionCache {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
|
|
62
|
+
function buildConnector ({ allowH2, preferH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
|
|
63
63
|
if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
|
|
64
64
|
throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
|
|
65
65
|
}
|
|
@@ -89,7 +89,7 @@ function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeo
|
|
|
89
89
|
servername,
|
|
90
90
|
session,
|
|
91
91
|
localAddress,
|
|
92
|
-
ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
|
|
92
|
+
ALPNProtocols: allowH2 ? (preferH2 ? ['h2', 'http/1.1'] : ['http/1.1', 'h2']) : ['http/1.1'],
|
|
93
93
|
socket: httpSocket, // upgrade socket connection
|
|
94
94
|
port,
|
|
95
95
|
host: hostname
|
|
@@ -142,7 +142,7 @@ function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeo
|
|
|
142
142
|
if (callback) {
|
|
143
143
|
const cb = callback
|
|
144
144
|
callback = null
|
|
145
|
-
cb(err)
|
|
145
|
+
cb(maybeNormalizeConnectError(err, this, { timeout, hostname, port }))
|
|
146
146
|
}
|
|
147
147
|
})
|
|
148
148
|
|
|
@@ -150,4 +150,29 @@ function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeo
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// `net.connect` with `autoSelectFamily` raises an `AggregateError` when every
|
|
154
|
+
// attempted address fails. If any of those failures is a timeout, surface the
|
|
155
|
+
// error as a `ConnectTimeoutError` so callers see the same error regardless of
|
|
156
|
+
// which timer (Node's internal one or undici's `connectTimeout`) wins the race.
|
|
157
|
+
// The original `AggregateError` is preserved on `.cause`.
|
|
158
|
+
function maybeNormalizeConnectError (err, socket, opts) {
|
|
159
|
+
if (
|
|
160
|
+
err instanceof AggregateError &&
|
|
161
|
+
(err.code === 'ETIMEDOUT' || err.errors.some((e) => e != null && e.code === 'ETIMEDOUT'))
|
|
162
|
+
) {
|
|
163
|
+
let message = 'Connect Timeout Error'
|
|
164
|
+
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
|
|
165
|
+
message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
|
|
166
|
+
} else {
|
|
167
|
+
message += ` (attempted address: ${opts.hostname}:${opts.port},`
|
|
168
|
+
}
|
|
169
|
+
message += ` timeout: ${opts.timeout}ms)`
|
|
170
|
+
|
|
171
|
+
const wrapped = new ConnectTimeoutError(message)
|
|
172
|
+
wrapped.cause = err
|
|
173
|
+
return wrapped
|
|
174
|
+
}
|
|
175
|
+
return err
|
|
176
|
+
}
|
|
177
|
+
|
|
153
178
|
module.exports = buildConnector
|
package/lib/core/symbols.js
CHANGED
|
@@ -62,6 +62,7 @@ module.exports = {
|
|
|
62
62
|
kListeners: Symbol('listeners'),
|
|
63
63
|
kHTTPContext: Symbol('http context'),
|
|
64
64
|
kMaxConcurrentStreams: Symbol('max concurrent streams'),
|
|
65
|
+
kHostAuthority: Symbol('host authority'),
|
|
65
66
|
kHTTP2InitialWindowSize: Symbol('http2 initial window size'),
|
|
66
67
|
kHTTP2ConnectionWindowSize: Symbol('http2 connection window size'),
|
|
67
68
|
kEnableConnectProtocol: Symbol('http2session connect protocol'),
|
package/lib/core/util.js
CHANGED
|
@@ -698,9 +698,8 @@ function isFormDataLike (object) {
|
|
|
698
698
|
}
|
|
699
699
|
|
|
700
700
|
function addAbortListener (signal, listener) {
|
|
701
|
-
if (signal
|
|
702
|
-
|
|
703
|
-
return () => disposable[Symbol.dispose]()
|
|
701
|
+
if (!signal || 'aborted' in signal) {
|
|
702
|
+
return addAbortListenerNative(signal, listener)[Symbol.dispose]
|
|
704
703
|
}
|
|
705
704
|
|
|
706
705
|
if (typeof signal.addEventListener === 'function') {
|
|
@@ -776,7 +775,7 @@ function isValidHeaderValue (characters) {
|
|
|
776
775
|
return !headerCharRegex.test(characters)
|
|
777
776
|
}
|
|
778
777
|
|
|
779
|
-
const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d
|
|
778
|
+
const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d+|\*)?$/
|
|
780
779
|
|
|
781
780
|
/**
|
|
782
781
|
* @typedef {object} RangeHeader
|
|
@@ -793,13 +792,14 @@ const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d+)?$/
|
|
|
793
792
|
*/
|
|
794
793
|
function parseRangeHeader (range) {
|
|
795
794
|
if (range == null || range === '') return { start: 0, end: null, size: null }
|
|
795
|
+
if (!range) return null
|
|
796
796
|
|
|
797
|
-
const m =
|
|
797
|
+
const m = rangeHeaderRegex.exec(range)
|
|
798
798
|
return m
|
|
799
799
|
? {
|
|
800
800
|
start: parseInt(m[1]),
|
|
801
801
|
end: m[2] ? parseInt(m[2]) : null,
|
|
802
|
-
size: m[3] ? parseInt(m[3]) : null
|
|
802
|
+
size: m[3] && m[3] !== '*' ? parseInt(m[3]) : null
|
|
803
803
|
}
|
|
804
804
|
: null
|
|
805
805
|
}
|
|
@@ -943,8 +943,10 @@ function getProtocolFromUrlString (urlString) {
|
|
|
943
943
|
return urlString.slice(0, urlString.indexOf(':') + 1)
|
|
944
944
|
}
|
|
945
945
|
|
|
946
|
-
const kEnumerableProperty =
|
|
947
|
-
|
|
946
|
+
const kEnumerableProperty = {
|
|
947
|
+
__proto__: null,
|
|
948
|
+
enumerable: true
|
|
949
|
+
}
|
|
948
950
|
|
|
949
951
|
const normalizedMethodRecordsBase = {
|
|
950
952
|
delete: 'DELETE',
|
|
@@ -360,16 +360,7 @@ class Parser {
|
|
|
360
360
|
this.paused = true
|
|
361
361
|
socket.unshift(data)
|
|
362
362
|
} else {
|
|
363
|
-
|
|
364
|
-
let message = ''
|
|
365
|
-
if (ptr) {
|
|
366
|
-
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
|
|
367
|
-
message =
|
|
368
|
-
'Response does not match the HTTP/1.1 protocol (' +
|
|
369
|
-
Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
|
|
370
|
-
')'
|
|
371
|
-
}
|
|
372
|
-
throw new HTTPParserError(message, constants.ERROR[ret], data)
|
|
363
|
+
throw this.createError(ret, data)
|
|
373
364
|
}
|
|
374
365
|
}
|
|
375
366
|
} catch (err) {
|
|
@@ -377,6 +368,54 @@ class Parser {
|
|
|
377
368
|
}
|
|
378
369
|
}
|
|
379
370
|
|
|
371
|
+
finish () {
|
|
372
|
+
assert(currentParser === null)
|
|
373
|
+
assert(this.ptr != null)
|
|
374
|
+
assert(!this.paused)
|
|
375
|
+
|
|
376
|
+
const { llhttp } = this
|
|
377
|
+
|
|
378
|
+
let ret
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
currentParser = this
|
|
382
|
+
ret = llhttp.llhttp_finish(this.ptr)
|
|
383
|
+
} finally {
|
|
384
|
+
currentParser = null
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (ret === constants.ERROR.OK) {
|
|
388
|
+
return null
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) {
|
|
392
|
+
this.paused = true
|
|
393
|
+
return null
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return this.createError(ret, EMPTY_BUF)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
createError (ret, data) {
|
|
400
|
+
const { llhttp, contentLength, bytesRead } = this
|
|
401
|
+
|
|
402
|
+
if (contentLength !== -1 && bytesRead !== contentLength) {
|
|
403
|
+
return new ResponseContentLengthMismatchError()
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const ptr = llhttp.llhttp_get_error_reason(this.ptr)
|
|
407
|
+
let message = ''
|
|
408
|
+
if (ptr) {
|
|
409
|
+
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
|
|
410
|
+
message =
|
|
411
|
+
'Response does not match the HTTP/1.1 protocol (' +
|
|
412
|
+
Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
|
|
413
|
+
')'
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return new HTTPParserError(message, constants.ERROR[ret], data)
|
|
417
|
+
}
|
|
418
|
+
|
|
380
419
|
destroy () {
|
|
381
420
|
assert(currentParser === null)
|
|
382
421
|
assert(this.ptr != null)
|
|
@@ -888,8 +927,11 @@ function onHttpSocketError (err) {
|
|
|
888
927
|
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
|
|
889
928
|
// to the user.
|
|
890
929
|
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
|
|
891
|
-
|
|
892
|
-
|
|
930
|
+
const parserErr = parser.finish()
|
|
931
|
+
if (parserErr) {
|
|
932
|
+
this[kError] = parserErr
|
|
933
|
+
this[kClient][kOnError](parserErr)
|
|
934
|
+
}
|
|
893
935
|
return
|
|
894
936
|
}
|
|
895
937
|
|
|
@@ -906,8 +948,10 @@ function onHttpSocketEnd () {
|
|
|
906
948
|
const parser = this[kParser]
|
|
907
949
|
|
|
908
950
|
if (parser.statusCode && !parser.shouldKeepAlive) {
|
|
909
|
-
|
|
910
|
-
|
|
951
|
+
const parserErr = parser.finish()
|
|
952
|
+
if (parserErr) {
|
|
953
|
+
util.destroy(this, parserErr)
|
|
954
|
+
}
|
|
911
955
|
return
|
|
912
956
|
}
|
|
913
957
|
|
|
@@ -919,8 +963,7 @@ function onHttpSocketClose () {
|
|
|
919
963
|
|
|
920
964
|
if (parser) {
|
|
921
965
|
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
|
|
922
|
-
|
|
923
|
-
parser.onMessageComplete()
|
|
966
|
+
this[kError] = parser.finish() || this[kError]
|
|
924
967
|
}
|
|
925
968
|
|
|
926
969
|
this[kParser].destroy()
|
|
@@ -1382,8 +1425,6 @@ function writeBuffer (abort, body, client, request, socket, contentLength, heade
|
|
|
1382
1425
|
* @returns {Promise<void>}
|
|
1383
1426
|
*/
|
|
1384
1427
|
async function writeBlob (abort, body, client, request, socket, contentLength, header, expectsPayload) {
|
|
1385
|
-
assert(contentLength === body.size, 'blob body must have content length')
|
|
1386
|
-
|
|
1387
1428
|
try {
|
|
1388
1429
|
if (contentLength != null && contentLength !== body.size) {
|
|
1389
1430
|
throw new RequestContentLengthMismatchError()
|