undici 7.0.0-alpha.6 → 7.0.0-alpha.8

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.
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const assert = require('node:assert')
4
- const DecoratorHandler = require('../handler/decorator-handler')
5
4
 
6
5
  /**
7
6
  * This takes care of revalidation requests we send to the origin. If we get
@@ -14,123 +13,97 @@ const DecoratorHandler = require('../handler/decorator-handler')
14
13
  *
15
14
  * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation
16
15
  *
17
- * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandlers} DispatchHandlers
18
- * @implements {DispatchHandlers}
16
+ * @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler}
19
17
  */
20
- class CacheRevalidationHandler extends DecoratorHandler {
18
+ class CacheRevalidationHandler {
21
19
  #successful = false
20
+
22
21
  /**
23
- * @type {((boolean) => void) | null}
22
+ * @type {((boolean, any) => void) | null}
24
23
  */
25
24
  #callback
25
+
26
26
  /**
27
- * @type {(import('../../types/dispatcher.d.ts').default.DispatchHandlers)}
27
+ * @type {(import('../../types/dispatcher.d.ts').default.DispatchHandler)}
28
28
  */
29
29
  #handler
30
30
 
31
- #abort
31
+ #context
32
+
33
+ /**
34
+ * @type {boolean}
35
+ */
36
+ #allowErrorStatusCodes
32
37
 
33
38
  /**
34
39
  * @param {(boolean) => void} callback Function to call if the cached value is valid
35
40
  * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
41
+ * @param {boolean} allowErrorStatusCodes
36
42
  */
37
- constructor (callback, handler) {
43
+ constructor (callback, handler, allowErrorStatusCodes) {
38
44
  if (typeof callback !== 'function') {
39
45
  throw new TypeError('callback must be a function')
40
46
  }
41
47
 
42
- super(handler)
43
-
44
48
  this.#callback = callback
45
49
  this.#handler = handler
50
+ this.#allowErrorStatusCodes = allowErrorStatusCodes
46
51
  }
47
52
 
48
- onConnect (abort) {
53
+ onRequestStart (_, context) {
49
54
  this.#successful = false
50
- this.#abort = abort
55
+ this.#context = context
51
56
  }
52
57
 
53
- /**
54
- * @see {DispatchHandlers.onHeaders}
55
- *
56
- * @param {number} statusCode
57
- * @param {Buffer[]} rawHeaders
58
- * @param {() => void} resume
59
- * @param {string} statusMessage
60
- * @returns {boolean}
61
- */
62
- onHeaders (
58
+ onRequestUpgrade (controller, statusCode, headers, socket) {
59
+ this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
60
+ }
61
+
62
+ onResponseStart (
63
+ controller,
63
64
  statusCode,
64
- rawHeaders,
65
- resume,
66
- statusMessage
65
+ statusMessage,
66
+ headers
67
67
  ) {
68
68
  assert(this.#callback != null)
69
69
 
70
70
  // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
71
- this.#successful = statusCode === 304
72
- this.#callback(this.#successful)
71
+ // https://datatracker.ietf.org/doc/html/rfc5861#section-4
72
+ this.#successful = statusCode === 304 ||
73
+ (this.#allowErrorStatusCodes && statusCode >= 500 && statusCode <= 504)
74
+ this.#callback(this.#successful, this.#context)
73
75
  this.#callback = null
74
76
 
75
77
  if (this.#successful) {
76
78
  return true
77
79
  }
78
80
 
79
- if (typeof this.#handler.onConnect === 'function') {
80
- this.#handler.onConnect(this.#abort)
81
- }
82
-
83
- if (typeof this.#handler.onHeaders === 'function') {
84
- return this.#handler.onHeaders(
85
- statusCode,
86
- rawHeaders,
87
- resume,
88
- statusMessage
89
- )
90
- }
91
-
92
- return true
81
+ this.#handler.onRequestStart?.(controller, this.#context)
82
+ this.#handler.onResponseStart?.(
83
+ controller,
84
+ statusCode,
85
+ statusMessage,
86
+ headers
87
+ )
93
88
  }
94
89
 
95
- /**
96
- * @see {DispatchHandlers.onData}
97
- *
98
- * @param {Buffer} chunk
99
- * @returns {boolean}
100
- */
101
- onData (chunk) {
90
+ onResponseData (controller, chunk) {
102
91
  if (this.#successful) {
103
- return true
104
- }
105
-
106
- if (typeof this.#handler.onData === 'function') {
107
- return this.#handler.onData(chunk)
92
+ return
108
93
  }
109
94
 
110
- return true
95
+ return this.#handler.onResponseData(controller, chunk)
111
96
  }
112
97
 
113
- /**
114
- * @see {DispatchHandlers.onComplete}
115
- *
116
- * @param {string[] | null} rawTrailers
117
- */
118
- onComplete (rawTrailers) {
98
+ onResponseEnd (controller, trailers) {
119
99
  if (this.#successful) {
120
100
  return
121
101
  }
122
102
 
123
- if (typeof this.#handler.onComplete === 'function') {
124
- this.#handler.onComplete(rawTrailers)
125
- }
103
+ this.#handler.onResponseEnd?.(controller, trailers)
126
104
  }
127
105
 
128
- /**
129
- * @see {DispatchHandlers.onError}
130
- *
131
- * @param {Error} err
132
- */
133
- onError (err) {
106
+ onResponseError (controller, err) {
134
107
  if (this.#successful) {
135
108
  return
136
109
  }
@@ -140,8 +113,8 @@ class CacheRevalidationHandler extends DecoratorHandler {
140
113
  this.#callback = null
141
114
  }
142
115
 
143
- if (typeof this.#handler.onError === 'function') {
144
- this.#handler.onError(err)
116
+ if (typeof this.#handler.onResponseError === 'function') {
117
+ this.#handler.onResponseError(controller, err)
145
118
  } else {
146
119
  throw err
147
120
  }
@@ -2,6 +2,9 @@
2
2
 
3
3
  const assert = require('node:assert')
4
4
 
5
+ /**
6
+ * @deprecated
7
+ */
5
8
  module.exports = class DecoratorHandler {
6
9
  #handler
7
10
  #onCompleteCalled = false
@@ -10,6 +10,8 @@ const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
10
10
 
11
11
  const kBody = Symbol('body')
12
12
 
13
+ const noop = () => {}
14
+
13
15
  class BodyAsyncIterable {
14
16
  constructor (body) {
15
17
  this[kBody] = body
@@ -38,16 +40,12 @@ class RedirectHandler {
38
40
  throw new InvalidArgumentError('maxRedirections must be a positive number')
39
41
  }
40
42
 
41
- util.assertRequestHandler(handler, opts.method, opts.upgrade)
42
-
43
43
  this.dispatch = dispatch
44
44
  this.location = null
45
- this.abort = null
46
45
  this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy
47
46
  this.maxRedirections = maxRedirections
48
47
  this.handler = handler
49
48
  this.history = []
50
- this.redirectionLimitReached = false
51
49
 
52
50
  if (util.isStream(this.opts.body)) {
53
51
  // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
@@ -84,40 +82,51 @@ class RedirectHandler {
84
82
  }
85
83
  }
86
84
 
87
- onConnect (abort) {
88
- this.abort = abort
89
- this.handler.onConnect(abort, { history: this.history })
85
+ onRequestStart (controller, context) {
86
+ this.handler.onRequestStart?.(controller, { ...context, history: this.history })
90
87
  }
91
88
 
92
- onUpgrade (statusCode, headers, socket) {
93
- this.handler.onUpgrade(statusCode, headers, socket)
89
+ onRequestUpgrade (controller, statusCode, headers, socket) {
90
+ this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
94
91
  }
95
92
 
96
- onError (error) {
97
- this.handler.onError(error)
98
- }
99
-
100
- onHeaders (statusCode, headers, resume, statusText) {
101
- this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body)
102
- ? null
103
- : parseLocation(statusCode, headers)
104
-
93
+ onResponseStart (controller, statusCode, statusMessage, headers) {
105
94
  if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) {
106
- if (this.request) {
107
- this.request.abort(new Error('max redirects'))
95
+ throw new Error('max redirects')
96
+ }
97
+
98
+ // https://tools.ietf.org/html/rfc7231#section-6.4.2
99
+ // https://fetch.spec.whatwg.org/#http-redirect-fetch
100
+ // In case of HTTP 301 or 302 with POST, change the method to GET
101
+ if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') {
102
+ this.opts.method = 'GET'
103
+ if (util.isStream(this.opts.body)) {
104
+ util.destroy(this.opts.body.on('error', noop))
108
105
  }
106
+ this.opts.body = null
107
+ }
109
108
 
110
- this.redirectionLimitReached = true
111
- this.abort(new Error('max redirects'))
112
- return
109
+ // https://tools.ietf.org/html/rfc7231#section-6.4.4
110
+ // In case of HTTP 303, always replace method to be either HEAD or GET
111
+ if (statusCode === 303 && this.opts.method !== 'HEAD') {
112
+ this.opts.method = 'GET'
113
+ if (util.isStream(this.opts.body)) {
114
+ util.destroy(this.opts.body.on('error', noop))
115
+ }
116
+ this.opts.body = null
113
117
  }
114
118
 
119
+ this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) || redirectableStatusCodes.indexOf(statusCode) === -1
120
+ ? null
121
+ : headers.location
122
+
115
123
  if (this.opts.origin) {
116
124
  this.history.push(new URL(this.opts.path, this.opts.origin))
117
125
  }
118
126
 
119
127
  if (!this.location) {
120
- return this.handler.onHeaders(statusCode, headers, resume, statusText)
128
+ this.handler.onResponseStart?.(controller, statusCode, statusMessage, headers)
129
+ return
121
130
  }
122
131
 
123
132
  const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
@@ -131,16 +140,9 @@ class RedirectHandler {
131
140
  this.opts.origin = origin
132
141
  this.opts.maxRedirections = 0
133
142
  this.opts.query = null
134
-
135
- // https://tools.ietf.org/html/rfc7231#section-6.4.4
136
- // In case of HTTP 303, always replace method to be either HEAD or GET
137
- if (statusCode === 303 && this.opts.method !== 'HEAD') {
138
- this.opts.method = 'GET'
139
- this.opts.body = null
140
- }
141
143
  }
142
144
 
143
- onData (chunk) {
145
+ onResponseData (controller, chunk) {
144
146
  if (this.location) {
145
147
  /*
146
148
  https://tools.ietf.org/html/rfc7231#section-6.4
@@ -160,11 +162,11 @@ class RedirectHandler {
160
162
  servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it.
161
163
  */
162
164
  } else {
163
- return this.handler.onData(chunk)
165
+ this.handler.onResponseData?.(controller, chunk)
164
166
  }
165
167
  }
166
168
 
167
- onComplete (trailers) {
169
+ onResponseEnd (controller, trailers) {
168
170
  if (this.location) {
169
171
  /*
170
172
  https://tools.ietf.org/html/rfc7231#section-6.4
@@ -174,32 +176,14 @@ class RedirectHandler {
174
176
 
175
177
  See comment on onData method above for more detailed information.
176
178
  */
177
-
178
- this.location = null
179
- this.abort = null
180
-
181
179
  this.dispatch(this.opts, this)
182
180
  } else {
183
- this.handler.onComplete(trailers)
181
+ this.handler.onResponseEnd(controller, trailers)
184
182
  }
185
183
  }
186
184
 
187
- onBodySent (chunk) {
188
- if (this.handler.onBodySent) {
189
- this.handler.onBodySent(chunk)
190
- }
191
- }
192
- }
193
-
194
- function parseLocation (statusCode, headers) {
195
- if (redirectableStatusCodes.indexOf(statusCode) === -1) {
196
- return null
197
- }
198
-
199
- for (let i = 0; i < headers.length; i += 2) {
200
- if (headers[i].length === 8 && util.headerNameToString(headers[i]) === 'location') {
201
- return headers[i + 1]
202
- }
185
+ onResponseError (controller, error) {
186
+ this.handler.onResponseError?.(controller, error)
203
187
  }
204
188
  }
205
189
 
@@ -37,6 +37,7 @@ class RetryHandler {
37
37
  this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }
38
38
  this.abort = null
39
39
  this.aborted = false
40
+ this.connectCalled = false
40
41
  this.retryOpts = {
41
42
  retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
42
43
  retryAfter: retryAfter ?? true,
@@ -68,16 +69,6 @@ class RetryHandler {
68
69
  this.end = null
69
70
  this.etag = null
70
71
  this.resume = null
71
-
72
- // Handle possible onConnect duplication
73
- this.handler.onConnect(reason => {
74
- this.aborted = true
75
- if (this.abort) {
76
- this.abort(reason)
77
- } else {
78
- this.reason = reason
79
- }
80
- })
81
72
  }
82
73
 
83
74
  onRequestSent () {
@@ -92,11 +83,14 @@ class RetryHandler {
92
83
  }
93
84
  }
94
85
 
95
- onConnect (abort) {
96
- if (this.aborted) {
97
- abort(this.reason)
98
- } else {
99
- this.abort = abort
86
+ onConnect (abort, context) {
87
+ this.abort = abort
88
+ if (!this.connectCalled) {
89
+ this.connectCalled = true
90
+ this.handler.onConnect(reason => {
91
+ this.aborted = true
92
+ this.abort(reason)
93
+ }, context)
100
94
  }
101
95
  }
102
96
 
@@ -0,0 +1,96 @@
1
+ 'use strict'
2
+
3
+ const { parseHeaders } = require('../core/util')
4
+ const { InvalidArgumentError } = require('../core/errors')
5
+
6
+ const kResume = Symbol('resume')
7
+
8
+ class UnwrapController {
9
+ #paused = false
10
+ #reason = null
11
+ #aborted = false
12
+ #abort
13
+
14
+ [kResume] = null
15
+
16
+ constructor (abort) {
17
+ this.#abort = abort
18
+ }
19
+
20
+ pause () {
21
+ this.#paused = true
22
+ }
23
+
24
+ resume () {
25
+ if (this.#paused) {
26
+ this.#paused = false
27
+ this[kResume]?.()
28
+ }
29
+ }
30
+
31
+ abort (reason) {
32
+ if (!this.#aborted) {
33
+ this.#aborted = true
34
+ this.#reason = reason
35
+ this.#abort(reason)
36
+ }
37
+ }
38
+
39
+ get aborted () {
40
+ return this.#aborted
41
+ }
42
+
43
+ get reason () {
44
+ return this.#reason
45
+ }
46
+
47
+ get paused () {
48
+ return this.#paused
49
+ }
50
+ }
51
+
52
+ module.exports = class UnwrapHandler {
53
+ #handler
54
+ #controller
55
+
56
+ constructor (handler) {
57
+ this.#handler = handler
58
+ }
59
+
60
+ static unwrap (handler) {
61
+ // TODO (fix): More checks...
62
+ return !handler.onRequestStart ? handler : new UnwrapHandler(handler)
63
+ }
64
+
65
+ onConnect (abort, context) {
66
+ this.#controller = new UnwrapController(abort)
67
+ this.#handler.onRequestStart?.(this.#controller, context)
68
+ }
69
+
70
+ onUpgrade (statusCode, rawHeaders, socket) {
71
+ this.#handler.onRequestUpgrade?.(this.#controller, statusCode, parseHeaders(rawHeaders), socket)
72
+ }
73
+
74
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
75
+ this.#controller[kResume] = resume
76
+ this.#handler.onResponseStart?.(this.#controller, statusCode, statusMessage, parseHeaders(rawHeaders))
77
+ return !this.#controller.paused
78
+ }
79
+
80
+ onData (data) {
81
+ this.#handler.onResponseData?.(this.#controller, data)
82
+ return !this.#controller.paused
83
+ }
84
+
85
+ onComplete (rawTrailers) {
86
+ this.#handler.onResponseEnd?.(this.#controller, parseHeaders(rawTrailers))
87
+ }
88
+
89
+ onError (err) {
90
+ if (!this.#handler.onResponseError) {
91
+ throw new InvalidArgumentError('invalid onError method')
92
+ }
93
+
94
+ this.#handler.onResponseError?.(this.#controller, err)
95
+ }
96
+ }
@@ -0,0 +1,98 @@
1
+ 'use strict'
2
+
3
+ const { InvalidArgumentError } = require('../core/errors')
4
+
5
+ module.exports = class WrapHandler {
6
+ #handler
7
+
8
+ constructor (handler) {
9
+ this.#handler = handler
10
+ }
11
+
12
+ static wrap (handler) {
13
+ // TODO (fix): More checks...
14
+ return handler.onRequestStart ? handler : new WrapHandler(handler)
15
+ }
16
+
17
+ // Unwrap Interface
18
+
19
+ onConnect (abort, context) {
20
+ return this.#handler.onConnect?.(abort, context)
21
+ }
22
+
23
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
24
+ return this.#handler.onHeaders?.(statusCode, rawHeaders, resume, statusMessage)
25
+ }
26
+
27
+ onUpgrade (statusCode, rawHeaders, socket) {
28
+ return this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
29
+ }
30
+
31
+ onData (data) {
32
+ return this.#handler.onData?.(data)
33
+ }
34
+
35
+ onComplete (trailers) {
36
+ return this.#handler.onComplete?.(trailers)
37
+ }
38
+
39
+ onError (err) {
40
+ if (!this.#handler.onError) {
41
+ throw new InvalidArgumentError('invalid onError method')
42
+ }
43
+
44
+ return this.#handler.onError?.(err)
45
+ }
46
+
47
+ // Wrap Interface
48
+
49
+ onRequestStart (controller, context) {
50
+ this.#handler.onConnect?.((reason) => controller.abort(reason), context)
51
+ }
52
+
53
+ onRequestUpgrade (controller, statusCode, headers, socket) {
54
+ const rawHeaders = []
55
+ for (const [key, val] of Object.entries(headers)) {
56
+ // TODO (fix): What if val is Array
57
+ rawHeaders.push(Buffer.from(key), Buffer.from(val))
58
+ }
59
+
60
+ this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
61
+ }
62
+
63
+ onResponseStart (controller, statusCode, statusMessage, headers) {
64
+ const rawHeaders = []
65
+ for (const [key, val] of Object.entries(headers)) {
66
+ // TODO (fix): What if val is Array
67
+ rawHeaders.push(Buffer.from(key), Buffer.from(val))
68
+ }
69
+
70
+ if (this.#handler.onHeaders?.(statusCode, rawHeaders, () => controller.resume(), statusMessage) === false) {
71
+ controller.pause()
72
+ }
73
+ }
74
+
75
+ onResponseData (controller, data) {
76
+ if (this.#handler.onData?.(data) === false) {
77
+ controller.pause()
78
+ }
79
+ }
80
+
81
+ onResponseEnd (controller, trailers) {
82
+ const rawTrailers = []
83
+ for (const [key, val] of Object.entries(trailers)) {
84
+ // TODO (fix): What if val is Array
85
+ rawTrailers.push(Buffer.from(key), Buffer.from(val))
86
+ }
87
+
88
+ this.#handler.onComplete?.(rawTrailers)
89
+ }
90
+
91
+ onResponseError (controller, err) {
92
+ if (!this.#handler.onError) {
93
+ throw new InvalidArgumentError('invalid onError method')
94
+ }
95
+
96
+ this.#handler.onError?.(err)
97
+ }
98
+ }