undici 7.0.0-alpha.1 → 7.0.0-alpha.10

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.
Files changed (113) hide show
  1. package/README.md +24 -38
  2. package/docs/docs/api/Agent.md +14 -14
  3. package/docs/docs/api/BalancedPool.md +16 -16
  4. package/docs/docs/api/CacheStore.md +131 -0
  5. package/docs/docs/api/Client.md +12 -12
  6. package/docs/docs/api/Debug.md +1 -1
  7. package/docs/docs/api/Dispatcher.md +98 -193
  8. package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
  9. package/docs/docs/api/MockAgent.md +5 -3
  10. package/docs/docs/api/MockClient.md +5 -5
  11. package/docs/docs/api/MockPool.md +4 -3
  12. package/docs/docs/api/Pool.md +15 -15
  13. package/docs/docs/api/PoolStats.md +1 -1
  14. package/docs/docs/api/ProxyAgent.md +3 -3
  15. package/docs/docs/api/RedirectHandler.md +1 -1
  16. package/docs/docs/api/RetryAgent.md +1 -1
  17. package/docs/docs/api/RetryHandler.md +4 -4
  18. package/docs/docs/api/WebSocket.md +46 -4
  19. package/docs/docs/api/api-lifecycle.md +11 -11
  20. package/docs/docs/best-practices/mocking-request.md +2 -2
  21. package/docs/docs/best-practices/proxy.md +1 -1
  22. package/index.d.ts +1 -1
  23. package/index.js +23 -3
  24. package/lib/api/abort-signal.js +2 -0
  25. package/lib/api/api-pipeline.js +4 -2
  26. package/lib/api/api-request.js +6 -4
  27. package/lib/api/api-stream.js +3 -1
  28. package/lib/api/api-upgrade.js +2 -2
  29. package/lib/api/readable.js +200 -47
  30. package/lib/api/util.js +2 -0
  31. package/lib/cache/memory-cache-store.js +177 -0
  32. package/lib/cache/sqlite-cache-store.js +446 -0
  33. package/lib/core/connect.js +54 -22
  34. package/lib/core/constants.js +35 -10
  35. package/lib/core/diagnostics.js +122 -128
  36. package/lib/core/errors.js +2 -2
  37. package/lib/core/request.js +6 -6
  38. package/lib/core/symbols.js +2 -0
  39. package/lib/core/tree.js +4 -2
  40. package/lib/core/util.js +238 -40
  41. package/lib/dispatcher/client-h1.js +405 -142
  42. package/lib/dispatcher/client-h2.js +212 -109
  43. package/lib/dispatcher/client.js +24 -7
  44. package/lib/dispatcher/dispatcher-base.js +4 -1
  45. package/lib/dispatcher/dispatcher.js +4 -0
  46. package/lib/dispatcher/fixed-queue.js +91 -49
  47. package/lib/dispatcher/pool-base.js +3 -3
  48. package/lib/dispatcher/pool-stats.js +2 -0
  49. package/lib/dispatcher/proxy-agent.js +3 -1
  50. package/lib/handler/cache-handler.js +393 -0
  51. package/lib/handler/cache-revalidation-handler.js +124 -0
  52. package/lib/handler/decorator-handler.js +3 -0
  53. package/lib/handler/redirect-handler.js +45 -59
  54. package/lib/handler/retry-handler.js +68 -109
  55. package/lib/handler/unwrap-handler.js +96 -0
  56. package/lib/handler/wrap-handler.js +98 -0
  57. package/lib/interceptor/cache.js +350 -0
  58. package/lib/interceptor/dns.js +375 -0
  59. package/lib/interceptor/response-error.js +15 -7
  60. package/lib/mock/mock-agent.js +5 -8
  61. package/lib/mock/mock-client.js +7 -2
  62. package/lib/mock/mock-errors.js +3 -1
  63. package/lib/mock/mock-interceptor.js +8 -6
  64. package/lib/mock/mock-pool.js +7 -2
  65. package/lib/mock/mock-symbols.js +2 -1
  66. package/lib/mock/mock-utils.js +33 -5
  67. package/lib/util/cache.js +360 -0
  68. package/lib/util/timers.js +50 -6
  69. package/lib/web/cache/cache.js +25 -21
  70. package/lib/web/cache/cachestorage.js +3 -1
  71. package/lib/web/cookies/index.js +18 -5
  72. package/lib/web/cookies/parse.js +6 -1
  73. package/lib/web/eventsource/eventsource.js +2 -0
  74. package/lib/web/fetch/body.js +43 -39
  75. package/lib/web/fetch/constants.js +45 -29
  76. package/lib/web/fetch/data-url.js +2 -2
  77. package/lib/web/fetch/formdata-parser.js +84 -46
  78. package/lib/web/fetch/formdata.js +42 -20
  79. package/lib/web/fetch/headers.js +119 -85
  80. package/lib/web/fetch/index.js +69 -65
  81. package/lib/web/fetch/request.js +132 -55
  82. package/lib/web/fetch/response.js +81 -36
  83. package/lib/web/fetch/util.js +274 -103
  84. package/lib/web/fetch/webidl.js +54 -18
  85. package/lib/web/websocket/connection.js +92 -15
  86. package/lib/web/websocket/constants.js +69 -9
  87. package/lib/web/websocket/events.js +8 -2
  88. package/lib/web/websocket/receiver.js +20 -26
  89. package/lib/web/websocket/stream/websocketerror.js +83 -0
  90. package/lib/web/websocket/stream/websocketstream.js +485 -0
  91. package/lib/web/websocket/util.js +115 -10
  92. package/lib/web/websocket/websocket.js +47 -170
  93. package/package.json +15 -11
  94. package/types/agent.d.ts +1 -1
  95. package/types/cache-interceptor.d.ts +172 -0
  96. package/types/cookies.d.ts +2 -0
  97. package/types/dispatcher.d.ts +29 -4
  98. package/types/env-http-proxy-agent.d.ts +1 -1
  99. package/types/fetch.d.ts +9 -8
  100. package/types/handlers.d.ts +4 -4
  101. package/types/index.d.ts +3 -1
  102. package/types/interceptors.d.ts +18 -1
  103. package/types/mock-agent.d.ts +4 -1
  104. package/types/mock-client.d.ts +1 -1
  105. package/types/mock-pool.d.ts +1 -1
  106. package/types/proxy-agent.d.ts +1 -1
  107. package/types/readable.d.ts +10 -7
  108. package/types/retry-handler.d.ts +3 -3
  109. package/types/webidl.d.ts +30 -4
  110. package/types/websocket.d.ts +33 -0
  111. package/lib/mock/pluralizer.js +0 -29
  112. package/lib/web/cache/symbols.js +0 -5
  113. package/lib/web/fetch/symbols.js +0 -8
@@ -0,0 +1,446 @@
1
+ 'use strict'
2
+
3
+ const { DatabaseSync } = require('node:sqlite')
4
+ const { Writable } = require('stream')
5
+ const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
6
+
7
+ const VERSION = 3
8
+
9
+ // 2gb
10
+ const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
11
+
12
+ /**
13
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
14
+ * @implements {CacheStore}
15
+ *
16
+ * @typedef {{
17
+ * id: Readonly<number>
18
+ * headers?: Record<string, string | string[]>
19
+ * vary?: string | object
20
+ * body: string
21
+ * } & import('../../types/cache-interceptor.d.ts').default.CacheValue} SqliteStoreValue
22
+ */
23
+ module.exports = class SqliteCacheStore {
24
+ #maxEntrySize = MAX_ENTRY_SIZE
25
+ #maxCount = Infinity
26
+
27
+ /**
28
+ * @type {import('node:sqlite').DatabaseSync}
29
+ */
30
+ #db
31
+
32
+ /**
33
+ * @type {import('node:sqlite').StatementSync}
34
+ */
35
+ #getValuesQuery
36
+
37
+ /**
38
+ * @type {import('node:sqlite').StatementSync}
39
+ */
40
+ #updateValueQuery
41
+
42
+ /**
43
+ * @type {import('node:sqlite').StatementSync}
44
+ */
45
+ #insertValueQuery
46
+
47
+ /**
48
+ * @type {import('node:sqlite').StatementSync}
49
+ */
50
+ #deleteExpiredValuesQuery
51
+
52
+ /**
53
+ * @type {import('node:sqlite').StatementSync}
54
+ */
55
+ #deleteByUrlQuery
56
+
57
+ /**
58
+ * @type {import('node:sqlite').StatementSync}
59
+ */
60
+ #countEntriesQuery
61
+
62
+ /**
63
+ * @type {import('node:sqlite').StatementSync}
64
+ */
65
+ #deleteOldValuesQuery
66
+
67
+ /**
68
+ * @param {import('../../types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts | undefined} opts
69
+ */
70
+ constructor (opts) {
71
+ if (opts) {
72
+ if (typeof opts !== 'object') {
73
+ throw new TypeError('SqliteCacheStore options must be an object')
74
+ }
75
+
76
+ if (opts.maxEntrySize !== undefined) {
77
+ if (
78
+ typeof opts.maxEntrySize !== 'number' ||
79
+ !Number.isInteger(opts.maxEntrySize) ||
80
+ opts.maxEntrySize < 0
81
+ ) {
82
+ throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer')
83
+ }
84
+
85
+ if (opts.maxEntrySize > MAX_ENTRY_SIZE) {
86
+ throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb')
87
+ }
88
+
89
+ this.#maxEntrySize = opts.maxEntrySize
90
+ }
91
+
92
+ if (opts.maxCount !== undefined) {
93
+ if (
94
+ typeof opts.maxCount !== 'number' ||
95
+ !Number.isInteger(opts.maxCount) ||
96
+ opts.maxCount < 0
97
+ ) {
98
+ throw new TypeError('SqliteCacheStore options.maxCount must be a non-negative integer')
99
+ }
100
+ this.#maxCount = opts.maxCount
101
+ }
102
+ }
103
+
104
+ this.#db = new DatabaseSync(opts?.location ?? ':memory:')
105
+
106
+ this.#db.exec(`
107
+ CREATE TABLE IF NOT EXISTS cacheInterceptorV${VERSION} (
108
+ -- Data specific to us
109
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ url TEXT NOT NULL,
111
+ method TEXT NOT NULL,
112
+
113
+ -- Data returned to the interceptor
114
+ body BUF NULL,
115
+ deleteAt INTEGER NOT NULL,
116
+ statusCode INTEGER NOT NULL,
117
+ statusMessage TEXT NOT NULL,
118
+ headers TEXT NULL,
119
+ cacheControlDirectives TEXT NULL,
120
+ etag TEXT NULL,
121
+ vary TEXT NULL,
122
+ cachedAt INTEGER NOT NULL,
123
+ staleAt INTEGER NOT NULL
124
+ );
125
+
126
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_url ON cacheInterceptorV${VERSION}(url);
127
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_method ON cacheInterceptorV${VERSION}(method);
128
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteAt ON cacheInterceptorV${VERSION}(deleteAt);
129
+ `)
130
+
131
+ this.#getValuesQuery = this.#db.prepare(`
132
+ SELECT
133
+ id,
134
+ body,
135
+ deleteAt,
136
+ statusCode,
137
+ statusMessage,
138
+ headers,
139
+ etag,
140
+ cacheControlDirectives,
141
+ vary,
142
+ cachedAt,
143
+ staleAt
144
+ FROM cacheInterceptorV${VERSION}
145
+ WHERE
146
+ url = ?
147
+ AND method = ?
148
+ ORDER BY
149
+ deleteAt ASC
150
+ `)
151
+
152
+ this.#updateValueQuery = this.#db.prepare(`
153
+ UPDATE cacheInterceptorV${VERSION} SET
154
+ body = ?,
155
+ deleteAt = ?,
156
+ statusCode = ?,
157
+ statusMessage = ?,
158
+ headers = ?,
159
+ etag = ?,
160
+ cacheControlDirectives = ?,
161
+ cachedAt = ?,
162
+ staleAt = ?,
163
+ deleteAt = ?
164
+ WHERE
165
+ id = ?
166
+ `)
167
+
168
+ this.#insertValueQuery = this.#db.prepare(`
169
+ INSERT INTO cacheInterceptorV${VERSION} (
170
+ url,
171
+ method,
172
+ body,
173
+ deleteAt,
174
+ statusCode,
175
+ statusMessage,
176
+ headers,
177
+ etag,
178
+ cacheControlDirectives,
179
+ vary,
180
+ cachedAt,
181
+ staleAt,
182
+ deleteAt
183
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
184
+ `)
185
+
186
+ this.#deleteByUrlQuery = this.#db.prepare(
187
+ `DELETE FROM cacheInterceptorV${VERSION} WHERE url = ?`
188
+ )
189
+
190
+ this.#countEntriesQuery = this.#db.prepare(
191
+ `SELECT COUNT(*) AS total FROM cacheInterceptorV${VERSION}`
192
+ )
193
+
194
+ this.#deleteExpiredValuesQuery = this.#db.prepare(
195
+ `DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`
196
+ )
197
+
198
+ this.#deleteOldValuesQuery = this.#maxCount === Infinity
199
+ ? null
200
+ : this.#db.prepare(`
201
+ DELETE FROM cacheInterceptorV${VERSION}
202
+ WHERE id IN (
203
+ SELECT
204
+ id
205
+ FROM cacheInterceptorV${VERSION}
206
+ ORDER BY cachedAt DESC
207
+ LIMIT ?
208
+ )
209
+ `)
210
+ }
211
+
212
+ close () {
213
+ this.#db.close()
214
+ }
215
+
216
+ /**
217
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
218
+ * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
219
+ */
220
+ get (key) {
221
+ assertCacheKey(key)
222
+
223
+ const value = this.#findValue(key)
224
+
225
+ if (!value) {
226
+ return undefined
227
+ }
228
+
229
+ /**
230
+ * @type {import('../../types/cache-interceptor.d.ts').default.GetResult}
231
+ */
232
+ const result = {
233
+ body: Buffer.from(value.body),
234
+ statusCode: value.statusCode,
235
+ statusMessage: value.statusMessage,
236
+ headers: value.headers ? JSON.parse(value.headers) : undefined,
237
+ etag: value.etag ? value.etag : undefined,
238
+ vary: value.vary ?? undefined,
239
+ cacheControlDirectives: value.cacheControlDirectives
240
+ ? JSON.parse(value.cacheControlDirectives)
241
+ : undefined,
242
+ cachedAt: value.cachedAt,
243
+ staleAt: value.staleAt,
244
+ deleteAt: value.deleteAt
245
+ }
246
+
247
+ return result
248
+ }
249
+
250
+ /**
251
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
252
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} value
253
+ * @returns {Writable | undefined}
254
+ */
255
+ createWriteStream (key, value) {
256
+ assertCacheKey(key)
257
+ assertCacheValue(value)
258
+
259
+ const url = this.#makeValueUrl(key)
260
+ let size = 0
261
+ /**
262
+ * @type {Buffer[] | null}
263
+ */
264
+ const body = []
265
+ const store = this
266
+
267
+ return new Writable({
268
+ write (chunk, encoding, callback) {
269
+ if (typeof chunk === 'string') {
270
+ chunk = Buffer.from(chunk, encoding)
271
+ }
272
+
273
+ size += chunk.byteLength
274
+
275
+ if (size < store.#maxEntrySize) {
276
+ body.push(chunk)
277
+ } else {
278
+ this.destroy()
279
+ }
280
+
281
+ callback()
282
+ },
283
+ final (callback) {
284
+ const existingValue = store.#findValue(key, true)
285
+ if (existingValue) {
286
+ // Updating an existing response, let's overwrite it
287
+ store.#updateValueQuery.run(
288
+ Buffer.concat(body),
289
+ value.deleteAt,
290
+ value.statusCode,
291
+ value.statusMessage,
292
+ value.headers ? JSON.stringify(value.headers) : null,
293
+ value.etag ? value.etag : null,
294
+ value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
295
+ value.cachedAt,
296
+ value.staleAt,
297
+ value.deleteAt,
298
+ existingValue.id
299
+ )
300
+ } else {
301
+ store.#prune()
302
+ // New response, let's insert it
303
+ store.#insertValueQuery.run(
304
+ url,
305
+ key.method,
306
+ Buffer.concat(body),
307
+ value.deleteAt,
308
+ value.statusCode,
309
+ value.statusMessage,
310
+ value.headers ? JSON.stringify(value.headers) : null,
311
+ value.etag ? value.etag : null,
312
+ value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
313
+ value.vary ? JSON.stringify(value.vary) : null,
314
+ value.cachedAt,
315
+ value.staleAt,
316
+ value.deleteAt
317
+ )
318
+ }
319
+
320
+ callback()
321
+ }
322
+ })
323
+ }
324
+
325
+ /**
326
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
327
+ */
328
+ delete (key) {
329
+ if (typeof key !== 'object') {
330
+ throw new TypeError(`expected key to be object, got ${typeof key}`)
331
+ }
332
+
333
+ this.#deleteByUrlQuery.run(this.#makeValueUrl(key))
334
+ }
335
+
336
+ #prune () {
337
+ if (this.size <= this.#maxCount) {
338
+ return 0
339
+ }
340
+
341
+ {
342
+ const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes
343
+ if (removed > 0) {
344
+ return removed
345
+ }
346
+ }
347
+
348
+ {
349
+ const removed = this.#deleteOldValuesQuery.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes
350
+ if (removed > 0) {
351
+ return removed
352
+ }
353
+ }
354
+
355
+ return 0
356
+ }
357
+
358
+ /**
359
+ * Counts the number of rows in the cache
360
+ * @returns {Number}
361
+ */
362
+ get size () {
363
+ const { total } = this.#countEntriesQuery.get()
364
+ return total
365
+ }
366
+
367
+ /**
368
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
369
+ * @returns {string}
370
+ */
371
+ #makeValueUrl (key) {
372
+ return `${key.origin}/${key.path}`
373
+ }
374
+
375
+ /**
376
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
377
+ * @param {boolean} [canBeExpired=false]
378
+ * @returns {(SqliteStoreValue & { vary?: Record<string, string[]> }) | undefined}
379
+ */
380
+ #findValue (key, canBeExpired = false) {
381
+ const url = this.#makeValueUrl(key)
382
+ const { headers, method } = key
383
+
384
+ /**
385
+ * @type {SqliteStoreValue[]}
386
+ */
387
+ const values = this.#getValuesQuery.all(url, method)
388
+
389
+ if (values.length === 0) {
390
+ return undefined
391
+ }
392
+
393
+ const now = Date.now()
394
+ for (const value of values) {
395
+ if (now >= value.deleteAt && !canBeExpired) {
396
+ return undefined
397
+ }
398
+
399
+ let matches = true
400
+
401
+ if (value.vary) {
402
+ if (!headers) {
403
+ return undefined
404
+ }
405
+
406
+ value.vary = JSON.parse(value.vary)
407
+
408
+ for (const header in value.vary) {
409
+ if (!headerValueEquals(headers[header], value.vary[header])) {
410
+ matches = false
411
+ break
412
+ }
413
+ }
414
+ }
415
+
416
+ if (matches) {
417
+ return value
418
+ }
419
+ }
420
+
421
+ return undefined
422
+ }
423
+ }
424
+
425
+ /**
426
+ * @param {string|string[]|null|undefined} lhs
427
+ * @param {string|string[]|null|undefined} rhs
428
+ * @returns {boolean}
429
+ */
430
+ function headerValueEquals (lhs, rhs) {
431
+ if (Array.isArray(lhs) && Array.isArray(rhs)) {
432
+ if (lhs.length !== rhs.length) {
433
+ return false
434
+ }
435
+
436
+ for (let i = 0; i < lhs.length; i++) {
437
+ if (rhs.includes(lhs[i])) {
438
+ return false
439
+ }
440
+ }
441
+
442
+ return true
443
+ }
444
+
445
+ return lhs === rhs
446
+ }
@@ -6,6 +6,8 @@ const util = require('./util')
6
6
  const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
7
7
  const timers = require('../util/timers')
8
8
 
9
+ function noop () {}
10
+
9
11
  let tls // include tls conditionally since it is not always available
10
12
 
11
13
  // TODO: session re-use does not wait for the first
@@ -92,9 +94,11 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
92
94
  servername = servername || options.servername || util.getServerName(host) || null
93
95
 
94
96
  const sessionKey = servername || hostname
97
+ assert(sessionKey)
98
+
95
99
  const session = customSession || sessionCache.get(sessionKey) || null
96
100
 
97
- assert(sessionKey)
101
+ port = port || 443
98
102
 
99
103
  socket = tls.connect({
100
104
  highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
@@ -105,7 +109,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
105
109
  // TODO(HTTP/2): Add support for h2c
106
110
  ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
107
111
  socket: httpSocket, // upgrade socket connection
108
- port: port || 443,
112
+ port,
109
113
  host: hostname
110
114
  })
111
115
 
@@ -116,11 +120,14 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
116
120
  })
117
121
  } else {
118
122
  assert(!httpSocket, 'httpSocket can only be sent on TLS update')
123
+
124
+ port = port || 80
125
+
119
126
  socket = net.connect({
120
127
  highWaterMark: 64 * 1024, // Same as nodejs fs streams.
121
128
  ...options,
122
129
  localAddress,
123
- port: port || 80,
130
+ port,
124
131
  host: hostname
125
132
  })
126
133
  }
@@ -131,12 +138,12 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
131
138
  socket.setKeepAlive(true, keepAliveInitialDelay)
132
139
  }
133
140
 
134
- const cancelConnectTimeout = setupConnectTimeout(new WeakRef(socket), timeout)
141
+ const clearConnectTimeout = setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port })
135
142
 
136
143
  socket
137
144
  .setNoDelay(true)
138
145
  .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
139
- cancelConnectTimeout()
146
+ queueMicrotask(clearConnectTimeout)
140
147
 
141
148
  if (callback) {
142
149
  const cb = callback
@@ -145,7 +152,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
145
152
  }
146
153
  })
147
154
  .on('error', function (err) {
148
- cancelConnectTimeout()
155
+ queueMicrotask(clearConnectTimeout)
149
156
 
150
157
  if (callback) {
151
158
  const cb = callback
@@ -158,50 +165,75 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
158
165
  }
159
166
  }
160
167
 
168
+ /**
169
+ * @param {WeakRef<net.Socket>} socketWeakRef
170
+ * @param {object} opts
171
+ * @param {number} opts.timeout
172
+ * @param {string} opts.hostname
173
+ * @param {number} opts.port
174
+ * @returns {() => void}
175
+ */
161
176
  const setupConnectTimeout = process.platform === 'win32'
162
- ? (socket, timeout) => {
163
- if (!timeout) {
164
- return () => { }
177
+ ? (socketWeakRef, opts) => {
178
+ if (!opts.timeout) {
179
+ return noop
165
180
  }
166
181
 
167
182
  let s1 = null
168
183
  let s2 = null
169
- const timer = timers.setTimeout(() => {
184
+ const fastTimer = timers.setFastTimeout(() => {
170
185
  // setImmediate is added to make sure that we prioritize socket error events over timeouts
171
186
  s1 = setImmediate(() => {
172
187
  // Windows needs an extra setImmediate probably due to implementation differences in the socket logic
173
- s2 = setImmediate(() => onConnectTimeout(socket.deref()))
188
+ s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts))
174
189
  })
175
- }, timeout)
190
+ }, opts.timeout)
176
191
  return () => {
177
- timers.clearTimeout(timer)
192
+ timers.clearFastTimeout(fastTimer)
178
193
  clearImmediate(s1)
179
194
  clearImmediate(s2)
180
195
  }
181
196
  }
182
- : (socket, timeout) => {
183
- if (!timeout) {
184
- return () => { }
197
+ : (socketWeakRef, opts) => {
198
+ if (!opts.timeout) {
199
+ return noop
185
200
  }
186
201
 
187
202
  let s1 = null
188
- const timer = timers.setTimeout(() => {
203
+ const fastTimer = timers.setFastTimeout(() => {
189
204
  // setImmediate is added to make sure that we prioritize socket error events over timeouts
190
205
  s1 = setImmediate(() => {
191
- onConnectTimeout(socket.deref())
206
+ onConnectTimeout(socketWeakRef.deref(), opts)
192
207
  })
193
- }, timeout)
208
+ }, opts.timeout)
194
209
  return () => {
195
- timers.clearTimeout(timer)
210
+ timers.clearFastTimeout(fastTimer)
196
211
  clearImmediate(s1)
197
212
  }
198
213
  }
199
214
 
200
- function onConnectTimeout (socket) {
215
+ /**
216
+ * @param {net.Socket} socket
217
+ * @param {object} opts
218
+ * @param {number} opts.timeout
219
+ * @param {string} opts.hostname
220
+ * @param {number} opts.port
221
+ */
222
+ function onConnectTimeout (socket, opts) {
223
+ // The socket could be already garbage collected
224
+ if (socket == null) {
225
+ return
226
+ }
227
+
201
228
  let message = 'Connect Timeout Error'
202
229
  if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
203
- message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')})`
230
+ message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
231
+ } else {
232
+ message += ` (attempted address: ${opts.hostname}:${opts.port},`
204
233
  }
234
+
235
+ message += ` timeout: ${opts.timeout}ms)`
236
+
205
237
  util.destroy(socket, new ConnectTimeoutError(message))
206
238
  }
207
239
 
@@ -1,10 +1,9 @@
1
1
  'use strict'
2
2
 
3
- /** @type {Record<string, string | undefined>} */
4
- const headerNameLowerCasedRecord = {}
5
-
6
- // https://developer.mozilla.org/docs/Web/HTTP/Headers
7
- const wellknownHeaderNames = [
3
+ /**
4
+ * @see https://developer.mozilla.org/docs/Web/HTTP/Headers
5
+ */
6
+ const wellknownHeaderNames = /** @type {const} */ ([
8
7
  'Accept',
9
8
  'Accept-Encoding',
10
9
  'Accept-Language',
@@ -100,7 +99,35 @@ const wellknownHeaderNames = [
100
99
  'X-Powered-By',
101
100
  'X-Requested-With',
102
101
  'X-XSS-Protection'
103
- ]
102
+ ])
103
+
104
+ /** @type {Record<typeof wellknownHeaderNames[number]|Lowercase<typeof wellknownHeaderNames[number]>, string>} */
105
+ const headerNameLowerCasedRecord = {}
106
+
107
+ // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
108
+ Object.setPrototypeOf(headerNameLowerCasedRecord, null)
109
+
110
+ /**
111
+ * @type {Record<Lowercase<typeof wellknownHeaderNames[number]>, Buffer>}
112
+ */
113
+ const wellknownHeaderNameBuffers = {}
114
+
115
+ // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
116
+ Object.setPrototypeOf(wellknownHeaderNameBuffers, null)
117
+
118
+ /**
119
+ * @param {string} header Lowercased header
120
+ * @returns {Buffer}
121
+ */
122
+ function getHeaderNameAsBuffer (header) {
123
+ let buffer = wellknownHeaderNameBuffers[header]
124
+
125
+ if (buffer === undefined) {
126
+ buffer = Buffer.from(header)
127
+ }
128
+
129
+ return buffer
130
+ }
104
131
 
105
132
  for (let i = 0; i < wellknownHeaderNames.length; ++i) {
106
133
  const key = wellknownHeaderNames[i]
@@ -109,10 +136,8 @@ for (let i = 0; i < wellknownHeaderNames.length; ++i) {
109
136
  lowerCasedKey
110
137
  }
111
138
 
112
- // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
113
- Object.setPrototypeOf(headerNameLowerCasedRecord, null)
114
-
115
139
  module.exports = {
116
140
  wellknownHeaderNames,
117
- headerNameLowerCasedRecord
141
+ headerNameLowerCasedRecord,
142
+ getHeaderNameAsBuffer
118
143
  }