undici 7.0.0-alpha.2 → 7.0.0-alpha.3

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 (37) hide show
  1. package/README.md +1 -1
  2. package/docs/docs/api/CacheStore.md +116 -0
  3. package/docs/docs/api/Dispatcher.md +10 -0
  4. package/index.js +6 -1
  5. package/lib/api/api-request.js +1 -1
  6. package/lib/api/readable.js +6 -6
  7. package/lib/cache/memory-cache-store.js +417 -0
  8. package/lib/core/constants.js +24 -1
  9. package/lib/core/util.js +41 -2
  10. package/lib/dispatcher/client-h1.js +100 -87
  11. package/lib/dispatcher/client-h2.js +127 -75
  12. package/lib/dispatcher/pool-base.js +3 -3
  13. package/lib/handler/cache-handler.js +359 -0
  14. package/lib/handler/cache-revalidation-handler.js +119 -0
  15. package/lib/interceptor/cache.js +171 -0
  16. package/lib/util/cache.js +224 -0
  17. package/lib/web/cache/cache.js +1 -0
  18. package/lib/web/cache/cachestorage.js +2 -0
  19. package/lib/web/eventsource/eventsource.js +2 -0
  20. package/lib/web/fetch/constants.js +12 -5
  21. package/lib/web/fetch/data-url.js +2 -2
  22. package/lib/web/fetch/formdata.js +3 -1
  23. package/lib/web/fetch/headers.js +2 -0
  24. package/lib/web/fetch/request.js +3 -1
  25. package/lib/web/fetch/response.js +3 -1
  26. package/lib/web/fetch/util.js +171 -47
  27. package/lib/web/fetch/webidl.js +16 -12
  28. package/lib/web/websocket/constants.js +67 -6
  29. package/lib/web/websocket/events.js +4 -0
  30. package/lib/web/websocket/stream/websocketerror.js +1 -1
  31. package/lib/web/websocket/websocket.js +2 -0
  32. package/package.json +7 -3
  33. package/types/cache-interceptor.d.ts +97 -0
  34. package/types/fetch.d.ts +9 -8
  35. package/types/index.d.ts +3 -0
  36. package/types/interceptors.d.ts +4 -0
  37. package/types/webidl.d.ts +7 -1
package/README.md CHANGED
@@ -132,7 +132,7 @@ Returns a promise with the result of the `Dispatcher.request` method.
132
132
 
133
133
  Calls `options.dispatcher.request(options)`.
134
134
 
135
- See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
135
+ See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./docs/examples/README.md) for examples.
136
136
 
137
137
  ### `undici.stream([url, options, ]factory): Promise`
138
138
 
@@ -0,0 +1,116 @@
1
+ # Cache Store
2
+
3
+ A Cache Store is responsible for storing and retrieving cached responses.
4
+ It is also responsible for deciding which specific response to use based off of
5
+ a response's `Vary` header (if present).
6
+
7
+ ## Pre-built Cache Stores
8
+
9
+ ### `MemoryCacheStore`
10
+
11
+ The `MemoryCacheStore` stores the responses in-memory.
12
+
13
+ **Options**
14
+
15
+ - `maxEntries` - The maximum amount of responses to store. Default `Infinity`.
16
+ - `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached.
17
+
18
+ ## Defining a Custom Cache Store
19
+
20
+ The store must implement the following functions:
21
+
22
+ ### Getter: `isFull`
23
+
24
+ This tells the cache interceptor if the store is full or not. If this is true,
25
+ the cache interceptor will not attempt to cache the response.
26
+
27
+ ### Function: `createReadStream`
28
+
29
+ Parameters:
30
+
31
+ * **req** `Dispatcher.RequestOptions` - Incoming request
32
+
33
+ Returns: `CacheStoreReadable | Promise<CacheStoreReadable | undefined> | undefined` - If the request is cached, a readable for the body is returned. Otherwise, `undefined` is returned.
34
+
35
+ ### Function: `createWriteStream`
36
+
37
+ Parameters:
38
+
39
+ * **req** `Dispatcher.RequestOptions` - Incoming request
40
+ * **value** `CacheStoreValue` - Response to store
41
+
42
+ Returns: `CacheStoreWriteable | undefined` - If the store is full, return `undefined`. Otherwise, return a writable so that the cache interceptor can stream the body and trailers to the store.
43
+
44
+ ## `CacheStoreValue`
45
+
46
+ This is an interface containing the majority of a response's data (minus the body).
47
+
48
+ ### Property `statusCode`
49
+
50
+ `number` - The response's HTTP status code.
51
+
52
+ ### Property `statusMessage`
53
+
54
+ `string` - The response's HTTP status message.
55
+
56
+ ### Property `rawHeaders`
57
+
58
+ `(Buffer | Buffer[])[]` - The response's headers.
59
+
60
+ ### Property `rawTrailers`
61
+
62
+ `string[] | undefined` - The response's trailers.
63
+
64
+ ### Property `vary`
65
+
66
+ `Record<string, string> | undefined` - The headers defined by the response's `Vary` header
67
+ and their respective values for later comparison
68
+
69
+ For example, for a response like
70
+ ```
71
+ Vary: content-encoding, accepts
72
+ content-encoding: utf8
73
+ accepts: application/json
74
+ ```
75
+
76
+ This would be
77
+ ```js
78
+ {
79
+ 'content-encoding': 'utf8',
80
+ accepts: 'application/json'
81
+ }
82
+ ```
83
+
84
+ ### Property `cachedAt`
85
+
86
+ `number` - Time in millis that this value was cached.
87
+
88
+ ### Property `staleAt`
89
+
90
+ `number` - Time in millis that this value is considered stale.
91
+
92
+ ### Property `deleteAt`
93
+
94
+ `number` - Time in millis that this value is to be deleted from the cache. This
95
+ is either the same sa staleAt or the `max-stale` caching directive.
96
+
97
+ The store must not return a response after the time defined in this property.
98
+
99
+ ## `CacheStoreReadable`
100
+
101
+ This extends Node's [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable)
102
+ and defines extra properties relevant to the cache interceptor.
103
+
104
+ ### Getter: `value`
105
+
106
+ The response's [`CacheStoreValue`](#cachestorevalue)
107
+
108
+ ## `CacheStoreWriteable`
109
+
110
+ This extends Node's [`Writable`](https://nodejs.org/api/stream.html#class-streamwritable)
111
+ and defines extra properties relevant to the cache interceptor.
112
+
113
+ ### Setter: `rawTrailers`
114
+
115
+ If the response has trailers, the cache interceptor will pass them to the cache
116
+ interceptor through this method.
@@ -1233,6 +1233,16 @@ test('should not error if request status code is not in the specified error code
1233
1233
 
1234
1234
  The Response Error Interceptor provides a robust mechanism for handling HTTP response errors by capturing detailed error information and propagating it through a structured `ResponseError` class. This enhancement improves error handling and debugging capabilities in applications using the interceptor.
1235
1235
 
1236
+ ##### `Cache Interceptor`
1237
+
1238
+ The `cache` interceptor implements client-side response caching as described in
1239
+ [RFC9111](https://www.rfc-editor.org/rfc/rfc9111.html).
1240
+
1241
+ **Options**
1242
+
1243
+ - `store` - The [`CacheStore`](./CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](./CacheStore.md#memorycachestore).
1244
+ - `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to cache the response of.
1245
+
1236
1246
  ## Instance Events
1237
1247
 
1238
1248
  ### Event: `'connect'`
package/index.js CHANGED
@@ -40,7 +40,12 @@ module.exports.interceptors = {
40
40
  redirect: require('./lib/interceptor/redirect'),
41
41
  retry: require('./lib/interceptor/retry'),
42
42
  dump: require('./lib/interceptor/dump'),
43
- dns: require('./lib/interceptor/dns')
43
+ dns: require('./lib/interceptor/dns'),
44
+ cache: require('./lib/interceptor/cache')
45
+ }
46
+
47
+ module.exports.cacheStores = {
48
+ MemoryCacheStore: require('./lib/cache/memory-cache-store')
44
49
  }
45
50
 
46
51
  module.exports.buildConnector = buildConnector
@@ -65,7 +65,7 @@ class RequestHandler extends AsyncResource {
65
65
  this.removeAbortListener = util.addAbortListener(signal, () => {
66
66
  this.reason = signal.reason ?? new RequestAbortedError()
67
67
  if (this.res) {
68
- util.destroy(this.res, this.reason)
68
+ util.destroy(this.res.on('error', noop), this.reason)
69
69
  } else if (this.abort) {
70
70
  this.abort(this.reason)
71
71
  }
@@ -164,7 +164,7 @@ class BodyReadable extends Readable {
164
164
  * @see https://fetch.spec.whatwg.org/#dom-body-text
165
165
  * @returns {Promise<string>}
166
166
  */
167
- async text () {
167
+ text () {
168
168
  return consume(this, 'text')
169
169
  }
170
170
 
@@ -174,7 +174,7 @@ class BodyReadable extends Readable {
174
174
  * @see https://fetch.spec.whatwg.org/#dom-body-json
175
175
  * @returns {Promise<unknown>}
176
176
  */
177
- async json () {
177
+ json () {
178
178
  return consume(this, 'json')
179
179
  }
180
180
 
@@ -184,7 +184,7 @@ class BodyReadable extends Readable {
184
184
  * @see https://fetch.spec.whatwg.org/#dom-body-blob
185
185
  * @returns {Promise<Blob>}
186
186
  */
187
- async blob () {
187
+ blob () {
188
188
  return consume(this, 'blob')
189
189
  }
190
190
 
@@ -194,7 +194,7 @@ class BodyReadable extends Readable {
194
194
  * @see https://fetch.spec.whatwg.org/#dom-body-bytes
195
195
  * @returns {Promise<Uint8Array>}
196
196
  */
197
- async bytes () {
197
+ bytes () {
198
198
  return consume(this, 'bytes')
199
199
  }
200
200
 
@@ -204,7 +204,7 @@ class BodyReadable extends Readable {
204
204
  * @see https://fetch.spec.whatwg.org/#dom-body-arraybuffer
205
205
  * @returns {Promise<ArrayBuffer>}
206
206
  */
207
- async arrayBuffer () {
207
+ arrayBuffer () {
208
208
  return consume(this, 'arrayBuffer')
209
209
  }
210
210
 
@@ -355,7 +355,7 @@ function isUnusable (bodyReadable) {
355
355
  * @param {string} type
356
356
  * @returns {Promise<any>}
357
357
  */
358
- async function consume (stream, type) {
358
+ function consume (stream, type) {
359
359
  assert(!stream[kConsume])
360
360
 
361
361
  return new Promise((resolve, reject) => {
@@ -0,0 +1,417 @@
1
+ 'use strict'
2
+
3
+ const { Writable, Readable } = require('node:stream')
4
+
5
+ /**
6
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
7
+ * @implements {CacheStore}
8
+ *
9
+ * @typedef {{
10
+ * readers: number
11
+ * readLock: boolean
12
+ * writeLock: boolean
13
+ * opts: import('../../types/cache-interceptor.d.ts').default.CacheStoreValue
14
+ * body: Buffer[]
15
+ * }} MemoryStoreValue
16
+ */
17
+ class MemoryCacheStore {
18
+ #maxEntries = Infinity
19
+
20
+ #maxEntrySize = Infinity
21
+
22
+ /**
23
+ * @type {((err) => void) | undefined}
24
+ */
25
+ #errorCallback = undefined
26
+
27
+ #entryCount = 0
28
+
29
+ /**
30
+ * @type {Map<string, Map<string, MemoryStoreValue>>}
31
+ */
32
+ #data = new Map()
33
+
34
+ /**
35
+ * @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
36
+ */
37
+ constructor (opts) {
38
+ if (opts) {
39
+ if (typeof opts !== 'object') {
40
+ throw new TypeError('MemoryCacheStore options must be an object')
41
+ }
42
+
43
+ if (opts.maxEntries !== undefined) {
44
+ if (
45
+ typeof opts.maxEntries !== 'number' ||
46
+ !Number.isInteger(opts.maxEntries) ||
47
+ opts.maxEntries < 0
48
+ ) {
49
+ throw new TypeError('MemoryCacheStore options.maxEntries must be a non-negative integer')
50
+ }
51
+ this.#maxEntries = opts.maxEntries
52
+ }
53
+
54
+ if (opts.maxEntrySize !== undefined) {
55
+ if (
56
+ typeof opts.maxEntrySize !== 'number' ||
57
+ !Number.isInteger(opts.maxEntrySize) ||
58
+ opts.maxEntrySize < 0
59
+ ) {
60
+ throw new TypeError('MemoryCacheStore options.maxEntrySize must be a non-negative integer')
61
+ }
62
+ this.#maxEntrySize = opts.maxEntrySize
63
+ }
64
+
65
+ if (opts.errorCallback !== undefined) {
66
+ if (typeof opts.errorCallback !== 'function') {
67
+ throw new TypeError('MemoryCacheStore options.errorCallback must be a function')
68
+ }
69
+ this.#errorCallback = opts.errorCallback
70
+ }
71
+ }
72
+ }
73
+
74
+ get isFull () {
75
+ return this.#entryCount >= this.#maxEntries
76
+ }
77
+
78
+ /**
79
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
80
+ * @returns {import('../../types/cache-interceptor.d.ts').default.CacheStoreReadable | undefined}
81
+ */
82
+ createReadStream (req) {
83
+ if (typeof req !== 'object') {
84
+ throw new TypeError(`expected req to be object, got ${typeof req}`)
85
+ }
86
+
87
+ const values = this.#getValuesForRequest(req, false)
88
+ if (!values) {
89
+ return undefined
90
+ }
91
+
92
+ const value = this.#findValue(req, values)
93
+
94
+ if (!value || value.readLock) {
95
+ return undefined
96
+ }
97
+
98
+ return new MemoryStoreReadableStream(value)
99
+ }
100
+
101
+ /**
102
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
103
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue} opts
104
+ * @returns {import('../../types/cache-interceptor.d.ts').default.CacheStoreWriteable | undefined}
105
+ */
106
+ createWriteStream (req, opts) {
107
+ if (typeof req !== 'object') {
108
+ throw new TypeError(`expected req to be object, got ${typeof req}`)
109
+ }
110
+ if (typeof opts !== 'object') {
111
+ throw new TypeError(`expected value to be object, got ${typeof opts}`)
112
+ }
113
+
114
+ if (this.isFull) {
115
+ return undefined
116
+ }
117
+
118
+ const values = this.#getValuesForRequest(req, true)
119
+
120
+ let value = this.#findValue(req, values)
121
+ if (!value) {
122
+ // The value doesn't already exist, meaning we haven't cached this
123
+ // response before. Let's assign it a value and insert it into our data
124
+ // property.
125
+
126
+ if (this.isFull) {
127
+ // Or not, we don't have space to add another response
128
+ return undefined
129
+ }
130
+
131
+ this.#entryCount++
132
+
133
+ value = {
134
+ readers: 0,
135
+ readLock: false,
136
+ writeLock: false,
137
+ opts,
138
+ body: []
139
+ }
140
+
141
+ // We want to sort our responses in decending order by their deleteAt
142
+ // timestamps so that deleting expired responses is faster
143
+ if (
144
+ values.length === 0 ||
145
+ opts.deleteAt < values[values.length - 1].deleteAt
146
+ ) {
147
+ // Our value is either the only response for this path or our deleteAt
148
+ // time is sooner than all the other responses
149
+ values.push(value)
150
+ } else if (opts.deleteAt >= values[0].deleteAt) {
151
+ // Our deleteAt is later than everyone elses
152
+ values.unshift(value)
153
+ } else {
154
+ // We're neither in the front or the end, let's just binary search to
155
+ // find our stop we need to be in
156
+ let startIndex = 0
157
+ let endIndex = values.length
158
+ while (true) {
159
+ if (startIndex === endIndex) {
160
+ values.splice(startIndex, 0, value)
161
+ break
162
+ }
163
+
164
+ const middleIndex = Math.floor((startIndex + endIndex) / 2)
165
+ const middleValue = values[middleIndex]
166
+ if (opts.deleteAt === middleIndex) {
167
+ values.splice(middleIndex, 0, value)
168
+ break
169
+ } else if (opts.deleteAt > middleValue.opts.deleteAt) {
170
+ endIndex = middleIndex
171
+ continue
172
+ } else {
173
+ startIndex = middleIndex
174
+ continue
175
+ }
176
+ }
177
+ }
178
+ } else {
179
+ // Check if there's already another request writing to the value or
180
+ // a request reading from it
181
+ if (value.writeLock || value.readLock) {
182
+ return undefined
183
+ }
184
+
185
+ // Empty it so we can overwrite it
186
+ value.body = []
187
+ }
188
+
189
+ const writable = new MemoryStoreWritableStream(
190
+ value,
191
+ this.#maxEntrySize
192
+ )
193
+
194
+ // Remove the value if there was some error
195
+ writable.on('error', (err) => {
196
+ values.filter(current => value !== current)
197
+ if (this.#errorCallback) {
198
+ this.#errorCallback(err)
199
+ }
200
+ })
201
+
202
+ writable.on('bodyOversized', () => {
203
+ values.filter(current => value !== current)
204
+ })
205
+
206
+ return writable
207
+ }
208
+
209
+ /**
210
+ * @param {string} origin
211
+ */
212
+ deleteByOrigin (origin) {
213
+ this.#data.delete(origin)
214
+ }
215
+
216
+ /**
217
+ * Gets all of the requests of the same origin, path, and method. Does not
218
+ * take the `vary` property into account.
219
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
220
+ * @param {boolean} [makeIfDoesntExist=false]
221
+ */
222
+ #getValuesForRequest (req, makeIfDoesntExist) {
223
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3
224
+ let cachedPaths = this.#data.get(req.origin)
225
+ if (!cachedPaths) {
226
+ if (!makeIfDoesntExist) {
227
+ return undefined
228
+ }
229
+
230
+ cachedPaths = new Map()
231
+ this.#data.set(req.origin, cachedPaths)
232
+ }
233
+
234
+ let values = cachedPaths.get(`${req.path}:${req.method}`)
235
+ if (!values && makeIfDoesntExist) {
236
+ values = []
237
+ cachedPaths.set(`${req.path}:${req.method}`, values)
238
+ }
239
+
240
+ return values
241
+ }
242
+
243
+ /**
244
+ * Given a list of values of a certain request, this decides the best value
245
+ * to respond with.
246
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
247
+ * @param {MemoryStoreValue[]} values
248
+ * @returns {MemoryStoreValue | undefined}
249
+ */
250
+ #findValue (req, values) {
251
+ /**
252
+ * @type {MemoryStoreValue}
253
+ */
254
+ let value
255
+ const now = Date.now()
256
+ for (let i = values.length - 1; i >= 0; i--) {
257
+ const current = values[i]
258
+ const currentCacheValue = current.opts
259
+ if (now >= currentCacheValue.deleteAt) {
260
+ // We've reached expired values, let's delete them
261
+ this.#entryCount -= values.length - i
262
+ values.length = i
263
+ break
264
+ }
265
+
266
+ let matches = true
267
+
268
+ if (currentCacheValue.vary) {
269
+ if (!req.headers) {
270
+ matches = false
271
+ break
272
+ }
273
+
274
+ for (const key in currentCacheValue.vary) {
275
+ if (currentCacheValue.vary[key] !== req.headers[key]) {
276
+ matches = false
277
+ break
278
+ }
279
+ }
280
+ }
281
+
282
+ if (matches) {
283
+ value = current
284
+ break
285
+ }
286
+ }
287
+
288
+ return value
289
+ }
290
+ }
291
+
292
+ class MemoryStoreReadableStream extends Readable {
293
+ /**
294
+ * @type {MemoryStoreValue}
295
+ */
296
+ #value
297
+ /**
298
+ * @type {Buffer[]}
299
+ */
300
+ #chunksToSend = []
301
+
302
+ /**
303
+ * @param {MemoryStoreValue} value
304
+ */
305
+ constructor (value) {
306
+ super()
307
+
308
+ if (value.readLock) {
309
+ throw new Error('can\'t read a locked value')
310
+ }
311
+
312
+ this.#value = value
313
+ this.#chunksToSend = value?.body ? [...value.body, null] : [null]
314
+
315
+ this.#value.readers++
316
+ this.#value.writeLock = true
317
+
318
+ this.on('close', () => {
319
+ this.#value.readers--
320
+
321
+ if (this.#value.readers === 0) {
322
+ this.#value.writeLock = false
323
+ }
324
+ })
325
+ }
326
+
327
+ get value () {
328
+ return this.#value.opts
329
+ }
330
+
331
+ /**
332
+ * @param {number} size
333
+ */
334
+ _read (size) {
335
+ if (this.#chunksToSend.length === 0) {
336
+ throw new Error('no chunks left to read, stream should have closed')
337
+ }
338
+
339
+ if (size > this.#chunksToSend.length) {
340
+ size = this.#chunksToSend.length
341
+ }
342
+
343
+ for (let i = 0; i < size; i++) {
344
+ this.push(this.#chunksToSend.shift())
345
+ }
346
+ }
347
+ }
348
+
349
+ class MemoryStoreWritableStream extends Writable {
350
+ /**
351
+ * @type {MemoryStoreValue}
352
+ */
353
+ #value
354
+ #currentSize = 0
355
+ #maxEntrySize = 0
356
+ /**
357
+ * @type {Buffer[]|null}
358
+ */
359
+ #body = []
360
+
361
+ /**
362
+ * @param {MemoryStoreValue} value
363
+ * @param {number} maxEntrySize
364
+ */
365
+ constructor (value, maxEntrySize) {
366
+ super()
367
+ this.#value = value
368
+ this.#value.readLock = true
369
+ this.#maxEntrySize = maxEntrySize ?? Infinity
370
+ }
371
+
372
+ get rawTrailers () {
373
+ return this.#value.opts.rawTrailers
374
+ }
375
+
376
+ /**
377
+ * @param {string[] | undefined} trailers
378
+ */
379
+ set rawTrailers (trailers) {
380
+ this.#value.opts.rawTrailers = trailers
381
+ }
382
+
383
+ /**
384
+ * @param {Buffer} chunk
385
+ * @param {string} encoding
386
+ * @param {BufferEncoding} encoding
387
+ */
388
+ _write (chunk, encoding, callback) {
389
+ if (typeof chunk === 'string') {
390
+ chunk = Buffer.from(chunk, encoding)
391
+ }
392
+
393
+ this.#currentSize += chunk.byteLength
394
+ if (this.#currentSize < this.#maxEntrySize) {
395
+ this.#body.push(chunk)
396
+ } else {
397
+ this.#body = null // release memory as early as possible
398
+ this.emit('bodyOversized')
399
+ }
400
+
401
+ callback()
402
+ }
403
+
404
+ /**
405
+ * @param {() => void} callback
406
+ */
407
+ _final (callback) {
408
+ if (this.#currentSize < this.#maxEntrySize) {
409
+ this.#value.readLock = false
410
+ this.#value.body = this.#body
411
+ }
412
+
413
+ callback()
414
+ }
415
+ }
416
+
417
+ module.exports = MemoryCacheStore
@@ -107,6 +107,28 @@ const headerNameLowerCasedRecord = {}
107
107
  // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
108
108
  Object.setPrototypeOf(headerNameLowerCasedRecord, null)
109
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
+ }
131
+
110
132
  for (let i = 0; i < wellknownHeaderNames.length; ++i) {
111
133
  const key = wellknownHeaderNames[i]
112
134
  const lowerCasedKey = key.toLowerCase()
@@ -116,5 +138,6 @@ for (let i = 0; i < wellknownHeaderNames.length; ++i) {
116
138
 
117
139
  module.exports = {
118
140
  wellknownHeaderNames,
119
- headerNameLowerCasedRecord
141
+ headerNameLowerCasedRecord,
142
+ getHeaderNameAsBuffer
120
143
  }