undici 5.27.2 → 5.28.1

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.
@@ -33,7 +33,7 @@ Returns: `Client`
33
33
  * **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.
34
34
  * **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.
35
35
  * **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
36
- * **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overriden by a SETTINGS remote frame.
36
+ * **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.
37
37
 
38
38
  #### Parameter: `ConnectOptions`
39
39
 
@@ -35,8 +35,7 @@ const mockPool = mockAgent.get('http://localhost:3000')
35
35
 
36
36
  ### `MockPool.intercept(options)`
37
37
 
38
- This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance, but each intercept is only used once.
39
- For example if you expect to make 2 requests inside a test, you need to call `intercept()` twice. Assuming you use `disableNetConnect()` you will get `MockNotMatchedError` on the second request when you only call `intercept()` once.
38
+ This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance, but each intercept is only used once. For example if you expect to make 2 requests inside a test, you need to call `intercept()` twice. Assuming you use `disableNetConnect()` you will get `MockNotMatchedError` on the second request when you only call `intercept()` once.
40
39
 
41
40
  When defining interception rules, all the rules must pass for a request to be intercepted. If a request is not intercepted, a real request will be attempted.
42
41
 
@@ -54,11 +53,11 @@ Returns: `MockInterceptor` corresponding to the input options.
54
53
 
55
54
  ### Parameter: `MockPoolInterceptOptions`
56
55
 
57
- * **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path.
56
+ * **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path. When a `RegExp` or callback is used, it will match against the request path including all query parameters in alphabetical order. When a `string` is provided, the query parameters can be conveniently specified through the `MockPoolInterceptOptions.query` setting.
58
57
  * **method** `string | RegExp | (method: string) => boolean` - (optional) - a matcher for the HTTP request method. Defaults to `GET`.
59
58
  * **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body.
60
59
  * **headers** `Record<string, string | RegExp | (body: string) => boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way.
61
- * **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params.
60
+ * **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params. Only applies when a `string` was provided for `MockPoolInterceptOptions.path`.
62
61
 
63
62
  ### Return: `MockInterceptor`
64
63
 
@@ -458,6 +457,41 @@ const result3 = await request('http://localhost:3000/foo')
458
457
  // Will not match and make attempt a real request
459
458
  ```
460
459
 
460
+ #### Example - Mocked request with path callback
461
+
462
+ ```js
463
+ import { MockAgent, setGlobalDispatcher, request } from 'undici'
464
+ import querystring from 'querystring'
465
+
466
+ const mockAgent = new MockAgent()
467
+ setGlobalDispatcher(mockAgent)
468
+
469
+ const mockPool = mockAgent.get('http://localhost:3000')
470
+
471
+ const matchPath = requestPath => {
472
+ const [pathname, search] = requestPath.split('?')
473
+ const requestQuery = querystring.parse(search)
474
+
475
+ if (!pathname.startsWith('/foo')) {
476
+ return false
477
+ }
478
+
479
+ if (!Object.keys(requestQuery).includes('foo') || requestQuery.foo !== 'bar') {
480
+ return false
481
+ }
482
+
483
+ return true
484
+ }
485
+
486
+ mockPool.intercept({
487
+ path: matchPath,
488
+ method: 'GET'
489
+ }).reply(200, 'foo')
490
+
491
+ const result = await request('http://localhost:3000/foo?foo=bar')
492
+ // Will match and return mocked data
493
+ ```
494
+
461
495
  ### `MockPool.close()`
462
496
 
463
497
  Closes the mock pool and de-registers from associated MockAgent.
@@ -0,0 +1,108 @@
1
+ # Class: RetryHandler
2
+
3
+ Extends: `undici.DispatcherHandlers`
4
+
5
+ A handler class that implements the retry logic for a request.
6
+
7
+ ## `new RetryHandler(dispatchOptions, retryHandlers, [retryOptions])`
8
+
9
+ Arguments:
10
+
11
+ - **options** `Dispatch.DispatchOptions & RetryOptions` (required) - It is an intersection of `Dispatcher.DispatchOptions` and `RetryOptions`.
12
+ - **retryHandlers** `RetryHandlers` (required) - Object containing the `dispatch` to be used on every retry, and `handler` for handling the `dispatch` lifecycle.
13
+
14
+ Returns: `retryHandler`
15
+
16
+ ### Parameter: `Dispatch.DispatchOptions & RetryOptions`
17
+
18
+ Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
19
+
20
+ #### `RetryOptions`
21
+
22
+ - **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
23
+ - **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
24
+ - **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
25
+ - **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
26
+ - **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2`
27
+ - **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true`
28
+ -
29
+ - **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']`
30
+ - **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
31
+ - **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN',
32
+
33
+ **`RetryContext`**
34
+
35
+ - `state`: `RetryState` - Current retry state. It can be mutated.
36
+ - `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler.
37
+
38
+ ### Parameter `RetryHandlers`
39
+
40
+ - **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry.
41
+ - **handler** Extends [`Dispatch.DispatchHandlers`](Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted.
42
+
43
+ Examples:
44
+
45
+ ```js
46
+ const client = new Client(`http://localhost:${server.address().port}`);
47
+ const chunks = [];
48
+ const handler = new RetryHandler(
49
+ {
50
+ ...dispatchOptions,
51
+ retryOptions: {
52
+ // custom retry function
53
+ retry: function (err, state, callback) {
54
+ counter++;
55
+
56
+ if (err.code && err.code === "UND_ERR_DESTROYED") {
57
+ callback(err);
58
+ return;
59
+ }
60
+
61
+ if (err.statusCode === 206) {
62
+ callback(err);
63
+ return;
64
+ }
65
+
66
+ setTimeout(() => callback(null), 1000);
67
+ },
68
+ },
69
+ },
70
+ {
71
+ dispatch: (...args) => {
72
+ return client.dispatch(...args);
73
+ },
74
+ handler: {
75
+ onConnect() {},
76
+ onBodySent() {},
77
+ onHeaders(status, _rawHeaders, resume, _statusMessage) {
78
+ // do something with headers
79
+ },
80
+ onData(chunk) {
81
+ chunks.push(chunk);
82
+ return true;
83
+ },
84
+ onComplete() {},
85
+ onError() {
86
+ // handle error properly
87
+ },
88
+ },
89
+ }
90
+ );
91
+ ```
92
+
93
+ #### Example - Basic RetryHandler with defaults
94
+
95
+ ```js
96
+ const client = new Client(`http://localhost:${server.address().port}`);
97
+ const handler = new RetryHandler(dispatchOptions, {
98
+ dispatch: client.dispatch.bind(client),
99
+ handler: {
100
+ onConnect() {},
101
+ onBodySent() {},
102
+ onHeaders(status, _rawHeaders, resume, _statusMessage) {},
103
+ onData(chunk) {},
104
+ onComplete() {},
105
+ onError(err) {},
106
+ },
107
+ });
108
+ ```
package/index.js CHANGED
@@ -15,6 +15,7 @@ const MockAgent = require('./lib/mock/mock-agent')
15
15
  const MockPool = require('./lib/mock/mock-pool')
16
16
  const mockErrors = require('./lib/mock/mock-errors')
17
17
  const ProxyAgent = require('./lib/proxy-agent')
18
+ const RetryHandler = require('./lib/handler/RetryHandler')
18
19
  const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
19
20
  const DecoratorHandler = require('./lib/handler/DecoratorHandler')
20
21
  const RedirectHandler = require('./lib/handler/RedirectHandler')
@@ -36,6 +37,7 @@ module.exports.Pool = Pool
36
37
  module.exports.BalancedPool = BalancedPool
37
38
  module.exports.Agent = Agent
38
39
  module.exports.ProxyAgent = ProxyAgent
40
+ module.exports.RetryHandler = RetryHandler
39
41
 
40
42
  module.exports.DecoratorHandler = DecoratorHandler
41
43
  module.exports.RedirectHandler = RedirectHandler
@@ -16,6 +16,8 @@ const kBody = Symbol('kBody')
16
16
  const kAbort = Symbol('abort')
17
17
  const kContentType = Symbol('kContentType')
18
18
 
19
+ const noop = () => {}
20
+
19
21
  module.exports = class BodyReadable extends Readable {
20
22
  constructor ({
21
23
  resume,
@@ -149,37 +151,50 @@ module.exports = class BodyReadable extends Readable {
149
151
  return this[kBody]
150
152
  }
151
153
 
152
- async dump (opts) {
154
+ dump (opts) {
153
155
  let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144
154
156
  const signal = opts && opts.signal
155
- const abortFn = () => {
156
- this.destroy()
157
- }
158
- let signalListenerCleanup
157
+
159
158
  if (signal) {
160
- if (typeof signal !== 'object' || !('aborted' in signal)) {
161
- throw new InvalidArgumentError('signal must be an AbortSignal')
162
- }
163
- util.throwIfAborted(signal)
164
- signalListenerCleanup = util.addAbortListener(signal, abortFn)
165
- }
166
- try {
167
- for await (const chunk of this) {
168
- util.throwIfAborted(signal)
169
- limit -= Buffer.byteLength(chunk)
170
- if (limit < 0) {
171
- return
159
+ try {
160
+ if (typeof signal !== 'object' || !('aborted' in signal)) {
161
+ throw new InvalidArgumentError('signal must be an AbortSignal')
172
162
  }
163
+ util.throwIfAborted(signal)
164
+ } catch (err) {
165
+ return Promise.reject(err)
173
166
  }
174
- } catch {
175
- util.throwIfAborted(signal)
176
- } finally {
177
- if (typeof signalListenerCleanup === 'function') {
178
- signalListenerCleanup()
179
- } else if (signalListenerCleanup) {
180
- signalListenerCleanup[Symbol.dispose]()
181
- }
182
167
  }
168
+
169
+ if (this.closed) {
170
+ return Promise.resolve(null)
171
+ }
172
+
173
+ return new Promise((resolve, reject) => {
174
+ const signalListenerCleanup = signal
175
+ ? util.addAbortListener(signal, () => {
176
+ this.destroy()
177
+ })
178
+ : noop
179
+
180
+ this
181
+ .on('close', function () {
182
+ signalListenerCleanup()
183
+ if (signal?.aborted) {
184
+ reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' }))
185
+ } else {
186
+ resolve(null)
187
+ }
188
+ })
189
+ .on('error', noop)
190
+ .on('data', function (chunk) {
191
+ limit -= chunk.length
192
+ if (limit <= 0) {
193
+ this.destroy()
194
+ }
195
+ })
196
+ .resume()
197
+ })
183
198
  }
184
199
  }
185
200
 
package/lib/client.js CHANGED
@@ -917,11 +917,9 @@ class Parser {
917
917
  socket[kReset] = true
918
918
  }
919
919
 
920
- let pause
921
- try {
922
- pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false
923
- } catch (err) {
924
- util.destroy(socket, err)
920
+ const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false
921
+
922
+ if (request.aborted) {
925
923
  return -1
926
924
  }
927
925
 
@@ -968,13 +966,8 @@ class Parser {
968
966
 
969
967
  this.bytesRead += buf.length
970
968
 
971
- try {
972
- if (request.onData(buf) === false) {
973
- return constants.ERROR.PAUSED
974
- }
975
- } catch (err) {
976
- util.destroy(socket, err)
977
- return -1
969
+ if (request.onData(buf) === false) {
970
+ return constants.ERROR.PAUSED
978
971
  }
979
972
  }
980
973
 
@@ -1015,11 +1008,7 @@ class Parser {
1015
1008
  return -1
1016
1009
  }
1017
1010
 
1018
- try {
1019
- request.onComplete(headers)
1020
- } catch (err) {
1021
- errorRequest(client, request, err)
1022
- }
1011
+ request.onComplete(headers)
1023
1012
 
1024
1013
  client[kQueue][client[kRunningIdx]++] = null
1025
1014
 
@@ -1183,7 +1172,7 @@ async function connect (client) {
1183
1172
  const idx = hostname.indexOf(']')
1184
1173
 
1185
1174
  assert(idx !== -1)
1186
- const ip = hostname.substr(1, idx - 1)
1175
+ const ip = hostname.substring(1, idx)
1187
1176
 
1188
1177
  assert(net.isIP(ip))
1189
1178
  hostname = ip
@@ -1682,6 +1671,7 @@ function writeH2 (client, session, request) {
1682
1671
  return false
1683
1672
  }
1684
1673
 
1674
+ /** @type {import('node:http2').ClientHttp2Stream} */
1685
1675
  let stream
1686
1676
  const h2State = client[kHTTP2SessionState]
1687
1677
 
@@ -1777,14 +1767,10 @@ function writeH2 (client, session, request) {
1777
1767
  const shouldEndStream = method === 'GET' || method === 'HEAD'
1778
1768
  if (expectContinue) {
1779
1769
  headers[HTTP2_HEADER_EXPECT] = '100-continue'
1780
- /**
1781
- * @type {import('node:http2').ClientHttp2Stream}
1782
- */
1783
1770
  stream = session.request(headers, { endStream: shouldEndStream, signal })
1784
1771
 
1785
1772
  stream.once('continue', writeBodyH2)
1786
1773
  } else {
1787
- /** @type {import('node:http2').ClientHttp2Stream} */
1788
1774
  stream = session.request(headers, {
1789
1775
  endStream: shouldEndStream,
1790
1776
  signal
@@ -1796,7 +1782,9 @@ function writeH2 (client, session, request) {
1796
1782
  ++h2State.openStreams
1797
1783
 
1798
1784
  stream.once('response', headers => {
1799
- if (request.onHeaders(Number(headers[HTTP2_HEADER_STATUS]), headers, stream.resume.bind(stream), '') === false) {
1785
+ const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
1786
+
1787
+ if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) {
1800
1788
  stream.pause()
1801
1789
  }
1802
1790
  })
@@ -1806,13 +1794,17 @@ function writeH2 (client, session, request) {
1806
1794
  })
1807
1795
 
1808
1796
  stream.on('data', (chunk) => {
1809
- if (request.onData(chunk) === false) stream.pause()
1797
+ if (request.onData(chunk) === false) {
1798
+ stream.pause()
1799
+ }
1810
1800
  })
1811
1801
 
1812
1802
  stream.once('close', () => {
1813
1803
  h2State.openStreams -= 1
1814
1804
  // TODO(HTTP/2): unref only if current streams count is 0
1815
- if (h2State.openStreams === 0) session.unref()
1805
+ if (h2State.openStreams === 0) {
1806
+ session.unref()
1807
+ }
1816
1808
  })
1817
1809
 
1818
1810
  stream.once('error', function (err) {
@@ -1972,7 +1964,11 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
1972
1964
  }
1973
1965
  }
1974
1966
  const onAbort = function () {
1975
- onFinished(new RequestAbortedError())
1967
+ if (finished) {
1968
+ return
1969
+ }
1970
+ const err = new RequestAbortedError()
1971
+ queueMicrotask(() => onFinished(err))
1976
1972
  }
1977
1973
  const onFinished = function (err) {
1978
1974
  if (finished) {
@@ -193,6 +193,19 @@ class ResponseExceededMaxSizeError extends UndiciError {
193
193
  }
194
194
  }
195
195
 
196
+ class RequestRetryError extends UndiciError {
197
+ constructor (message, code, { headers, data }) {
198
+ super(message)
199
+ Error.captureStackTrace(this, RequestRetryError)
200
+ this.name = 'RequestRetryError'
201
+ this.message = message || 'Request retry error'
202
+ this.code = 'UND_ERR_REQ_RETRY'
203
+ this.statusCode = code
204
+ this.data = data
205
+ this.headers = headers
206
+ }
207
+ }
208
+
196
209
  module.exports = {
197
210
  HTTPParserError,
198
211
  UndiciError,
@@ -212,5 +225,6 @@ module.exports = {
212
225
  NotSupportedError,
213
226
  ResponseContentLengthMismatchError,
214
227
  BalancedPoolMissingUpstreamError,
215
- ResponseExceededMaxSizeError
228
+ ResponseExceededMaxSizeError,
229
+ RequestRetryError
216
230
  }
@@ -230,9 +230,9 @@ class Request {
230
230
  onBodySent (chunk) {
231
231
  if (this[kHandler].onBodySent) {
232
232
  try {
233
- this[kHandler].onBodySent(chunk)
233
+ return this[kHandler].onBodySent(chunk)
234
234
  } catch (err) {
235
- this.onError(err)
235
+ this.abort(err)
236
236
  }
237
237
  }
238
238
  }
@@ -244,9 +244,9 @@ class Request {
244
244
 
245
245
  if (this[kHandler].onRequestSent) {
246
246
  try {
247
- this[kHandler].onRequestSent()
247
+ return this[kHandler].onRequestSent()
248
248
  } catch (err) {
249
- this.onError(err)
249
+ this.abort(err)
250
250
  }
251
251
  }
252
252
  }
@@ -271,14 +271,23 @@ class Request {
271
271
  channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
272
272
  }
273
273
 
274
- return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
274
+ try {
275
+ return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
276
+ } catch (err) {
277
+ this.abort(err)
278
+ }
275
279
  }
276
280
 
277
281
  onData (chunk) {
278
282
  assert(!this.aborted)
279
283
  assert(!this.completed)
280
284
 
281
- return this[kHandler].onData(chunk)
285
+ try {
286
+ return this[kHandler].onData(chunk)
287
+ } catch (err) {
288
+ this.abort(err)
289
+ return false
290
+ }
282
291
  }
283
292
 
284
293
  onUpgrade (statusCode, headers, socket) {
@@ -297,7 +306,13 @@ class Request {
297
306
  if (channels.trailers.hasSubscribers) {
298
307
  channels.trailers.publish({ request: this, trailers })
299
308
  }
300
- return this[kHandler].onComplete(trailers)
309
+
310
+ try {
311
+ return this[kHandler].onComplete(trailers)
312
+ } catch (err) {
313
+ // TODO (fix): This might be a bad idea?
314
+ this.onError(err)
315
+ }
301
316
  }
302
317
 
303
318
  onError (error) {
@@ -311,6 +326,7 @@ class Request {
311
326
  return
312
327
  }
313
328
  this.aborted = true
329
+
314
330
  return this[kHandler].onError(error)
315
331
  }
316
332
 
@@ -57,5 +57,6 @@ module.exports = {
57
57
  kHTTP2BuildRequest: Symbol('http2 build request'),
58
58
  kHTTP1BuildRequest: Symbol('http1 build request'),
59
59
  kHTTP2CopyHeaders: Symbol('http2 copy headers'),
60
- kHTTPConnVersion: Symbol('http connection version')
60
+ kHTTPConnVersion: Symbol('http connection version'),
61
+ kRetryHandlerDefaultRetry: Symbol('retry agent default retry')
61
62
  }
package/lib/core/util.js CHANGED
@@ -125,13 +125,13 @@ function getHostname (host) {
125
125
  const idx = host.indexOf(']')
126
126
 
127
127
  assert(idx !== -1)
128
- return host.substr(1, idx - 1)
128
+ return host.substring(1, idx)
129
129
  }
130
130
 
131
131
  const idx = host.indexOf(':')
132
132
  if (idx === -1) return host
133
133
 
134
- return host.substr(0, idx)
134
+ return host.substring(0, idx)
135
135
  }
136
136
 
137
137
  // IP addresses are not valid server names per RFC6066
@@ -228,7 +228,7 @@ function parseHeaders (headers, obj = {}) {
228
228
 
229
229
  if (!val) {
230
230
  if (Array.isArray(headers[i + 1])) {
231
- obj[key] = headers[i + 1]
231
+ obj[key] = headers[i + 1].map(x => x.toString('utf8'))
232
232
  } else {
233
233
  obj[key] = headers[i + 1].toString('utf8')
234
234
  }
@@ -431,16 +431,7 @@ function throwIfAborted (signal) {
431
431
  }
432
432
  }
433
433
 
434
- let events
435
434
  function addAbortListener (signal, listener) {
436
- if (typeof Symbol.dispose === 'symbol') {
437
- if (!events) {
438
- events = require('events')
439
- }
440
- if (typeof events.addAbortListener === 'function' && 'aborted' in signal) {
441
- return events.addAbortListener(signal, listener)
442
- }
443
- }
444
435
  if ('addEventListener' in signal) {
445
436
  signal.addEventListener('abort', listener, { once: true })
446
437
  return () => signal.removeEventListener('abort', listener)
@@ -464,6 +455,21 @@ function toUSVString (val) {
464
455
  return `${val}`
465
456
  }
466
457
 
458
+ // Parsed accordingly to RFC 9110
459
+ // https://www.rfc-editor.org/rfc/rfc9110#field.content-range
460
+ function parseRangeHeader (range) {
461
+ if (range == null || range === '') return { start: 0, end: null, size: null }
462
+
463
+ const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null
464
+ return m
465
+ ? {
466
+ start: parseInt(m[1]),
467
+ end: m[2] ? parseInt(m[2]) : null,
468
+ size: m[3] ? parseInt(m[3]) : null
469
+ }
470
+ : null
471
+ }
472
+
467
473
  const kEnumerableProperty = Object.create(null)
468
474
  kEnumerableProperty.enumerable = true
469
475
 
@@ -497,7 +503,9 @@ module.exports = {
497
503
  buildURL,
498
504
  throwIfAborted,
499
505
  addAbortListener,
506
+ parseRangeHeader,
500
507
  nodeMajor,
501
508
  nodeMinor,
502
- nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13)
509
+ nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13),
510
+ safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE']
503
511
  }