@nxtedition/nxt-undici 2.2.6 → 3.0.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/lib/index.js CHANGED
@@ -1,10 +1,5 @@
1
- import assert from 'node:assert'
2
- import createError from 'http-errors'
3
- import undici from 'undici'
4
- import { parseHeaders, AbortError, isStream } from './utils.js'
5
- import { BodyReadable } from './readable.js'
6
-
7
- export { parseHeaders } from './utils.js'
1
+ import undici from '@nxtedition/undici'
2
+ import { parseHeaders } from './utils.js'
8
3
 
9
4
  const dispatcherCache = new WeakMap()
10
5
 
@@ -18,6 +13,7 @@ export const interceptors = {
18
13
  proxy: (await import('./interceptor/proxy.js')).default,
19
14
  cache: (await import('./interceptor/cache.js')).default,
20
15
  requestId: (await import('./interceptor/request-id.js')).default,
16
+ dns: (await import('./interceptor/dns.js')).default,
21
17
  }
22
18
 
23
19
  export async function request(url, opts) {
@@ -55,180 +51,44 @@ export async function request(url, opts) {
55
51
  headers['user-agent'] = userAgent
56
52
  }
57
53
 
58
- if (method === 'CONNECT') {
59
- throw new createError.MethodNotAllowed()
60
- }
61
-
62
- // TODO (fix): Move into undici?
63
- if (
64
- headers != null &&
65
- (method === 'HEAD' || method === 'GET') &&
66
- (parseInt(headers['content-length']) > 0 || headers['transfer-encoding'])
67
- ) {
68
- throw new createError.BadRequest('HEAD and GET cannot have body')
69
- }
70
-
71
- // TODO (fix): Move into undici?
72
- if (
73
- opts.body != null &&
74
- (opts.body.size > 0 || opts.body.length > 0) &&
75
- (method === 'HEAD' || method === 'GET')
76
- ) {
77
- throw new createError.BadRequest('HEAD and GET cannot have body')
78
- }
79
-
80
- const expectsPayload = opts.method === 'PUT' || opts.method === 'POST' || opts.method === 'PATCH'
81
-
82
- if (headers != null && headers['content-length'] === '0' && !expectsPayload) {
83
- // https://tools.ietf.org/html/rfc7230#section-3.3.2
84
- // A user agent SHOULD NOT send a Content-Length header field when
85
- // the request message does not contain a payload body and the method
86
- // semantics do not anticipate such a body.
87
-
88
- // undici will error if provided an unexpected content-length: 0 header.
89
- delete headers['content-length']
90
- }
91
-
92
- if (isStream(opts.body)) {
93
- // TODO (fix): Remove this somehow?
94
- // Workaround: https://github.com/nodejs/undici/pull/2497
95
- opts.body.on('error', () => {})
54
+ const baseDispatcher = opts.dispatcher ?? undici.getGlobalDispatcher()
55
+
56
+ let dispatcher = dispatcherCache.get(baseDispatcher)
57
+ if (dispatcher == null) {
58
+ dispatcher = baseDispatcher.compose(
59
+ interceptors.responseError(),
60
+ interceptors.requestBodyFactory(),
61
+ interceptors.log(),
62
+ interceptors.dns(),
63
+ interceptors.requestId(),
64
+ interceptors.responseRetry(),
65
+ interceptors.responseVerify(),
66
+ interceptors.redirect(),
67
+ interceptors.cache(),
68
+ interceptors.proxy(),
69
+ )
70
+ dispatcherCache.set(baseDispatcher, dispatcher)
96
71
  }
97
72
 
98
- const dispatcher = opts.dispatcher ?? undici.getGlobalDispatcher()
99
-
100
- let dispatch = dispatcherCache.get(dispatcher)
101
- if (dispatch == null) {
102
- dispatch = (opts, handler) => dispatcher.dispatch(opts, handler)
103
- dispatch = interceptors.responseError(dispatch)
104
- dispatch = interceptors.requestBodyFactory(dispatch)
105
- dispatch = interceptors.log(dispatch)
106
- dispatch = interceptors.requestId(dispatch)
107
- dispatch = interceptors.responseRetry(dispatch)
108
- dispatch = interceptors.responseVerify(dispatch)
109
- dispatch = interceptors.redirect(dispatch)
110
- dispatch = interceptors.cache(dispatch)
111
- dispatch = interceptors.proxy(dispatch)
112
- dispatcherCache.set(dispatcher, dispatch)
113
- }
114
-
115
- return await new Promise((resolve, reject) =>
116
- dispatch(
117
- {
118
- id: opts.id,
119
- url,
120
- method,
121
- body: opts.body,
122
- query: opts.query,
123
- headers,
124
- origin: url.origin,
125
- path: url.path ?? (url.search ? `${url.pathname}${url.search ?? ''}` : url.pathname),
126
- reset: opts.reset ?? false,
127
- blocking: opts.blocking ?? false,
128
- headersTimeout: opts.headersTimeout,
129
- bodyTimeout: opts.bodyTimeout,
130
- idempotent: opts.idempotent,
131
- retry: opts.retry ?? 8,
132
- proxy: opts.proxy ?? false,
133
- cache: opts.cache ?? false,
134
- upgrade: opts.upgrade ?? false,
135
- follow: opts.follow ?? 8,
136
- error: opts.error ?? true,
137
- verify: opts.verify ?? true,
138
- logger: opts.logger ?? null,
139
- startTime: performance.now(),
140
- },
141
- {
142
- resolve,
143
- reject,
144
- method,
145
- highWaterMark: opts.highWaterMark ?? 128 * 1024,
146
- logger: opts.logger,
147
- signal: opts.signal,
148
- /** @type {Function | null} */ abort: null,
149
- /** @type {stream.Readable | null} */ body: null,
150
- onConnect(abort) {
151
- if (this.signal?.aborted) {
152
- abort(this.signal.reason)
153
- } else {
154
- this.abort = abort
155
-
156
- if (this.signal) {
157
- this.onAbort = () => {
158
- if (this.body) {
159
- this.body.destroy(this.signal.reason ?? new AbortError())
160
- } else {
161
- this.abort(this.signal.reason)
162
- }
163
- }
164
- this.signal.addEventListener('abort', this.onAbort)
165
- }
166
- }
167
- },
168
- onUpgrade(statusCode, rawHeaders, socket, headers = parseHeaders(rawHeaders)) {
169
- if (statusCode !== 101) {
170
- this.abort(createError(statusCode, { headers }))
171
- } else {
172
- this.resolve({ headers, socket })
173
- this.resolve = null
174
- }
175
- },
176
- onHeaders(
177
- statusCode,
178
- rawHeaders,
179
- resume,
180
- statusMessage,
181
- headers = parseHeaders(rawHeaders),
182
- ) {
183
- assert(statusCode >= 200)
184
-
185
- const contentLength = headers['content-length']
186
- const contentType = headers['content-type']
187
-
188
- this.body = new BodyReadable(this, {
189
- resume,
190
- abort: this.abort,
191
- highWaterMark: this.highWaterMark,
192
- method: this.method,
193
- statusCode,
194
- statusMessage,
195
- contentType: typeof contentType === 'string' ? contentType : undefined,
196
- headers,
197
- size: Number.isFinite(contentLength) ? contentLength : null,
198
- })
199
-
200
- if (this.signal) {
201
- this.body.on('close', () => {
202
- this.signal?.removeEventListener('abort', this.onAbort)
203
- this.signal = null
204
- })
205
- }
206
-
207
- this.resolve(this.body)
208
- this.resolve = null
209
- this.reject = null
210
-
211
- return true
212
- },
213
- onData(chunk) {
214
- return this.body.push(chunk)
215
- },
216
- onComplete() {
217
- this.body.push(null)
218
- },
219
- onError(err) {
220
- this.signal?.removeEventListener('abort', this.onAbort)
221
- this.signal = null
222
-
223
- if (this.body) {
224
- this.body.destroy(err)
225
- } else {
226
- this.reject(err)
227
- this.resolve = null
228
- this.reject = null
229
- }
230
- },
231
- },
232
- ),
233
- )
73
+ return await undici.request(url, {
74
+ method,
75
+ dispatcher,
76
+ body: opts.body,
77
+ query: opts.query,
78
+ headers,
79
+ signal: opts.signal,
80
+ reset: opts.reset ?? false,
81
+ blocking: opts.blocking ?? false,
82
+ headersTimeout: opts.headersTimeout,
83
+ bodyTimeout: opts.bodyTimeout,
84
+ idempotent: opts.idempotent,
85
+ retry: opts.retry ?? 8,
86
+ proxy: opts.proxy ?? false,
87
+ cache: opts.cache ?? false,
88
+ upgrade: opts.upgrade ?? false,
89
+ follow: opts.follow ?? 8,
90
+ error: opts.error ?? true,
91
+ verify: opts.verify ?? true,
92
+ logger: opts.logger ?? null,
93
+ })
234
94
  }
@@ -1,7 +1,6 @@
1
1
  import assert from 'node:assert'
2
2
  import { LRUCache } from 'lru-cache'
3
- import { parseHeaders, parseCacheControl } from '../utils.js'
4
- import { DecoratorHandler } from 'undici'
3
+ import { DecoratorHandler, parseHeaders, parseCacheControl } from '../utils.js'
5
4
 
6
5
  class CacheHandler extends DecoratorHandler {
7
6
  #handler
@@ -121,7 +120,7 @@ function makeKey(opts) {
121
120
 
122
121
  const DEFAULT_CACHE_STORE = new CacheStore({ maxSize: 128 * 1024, maxEntrySize: 1024 })
123
122
 
124
- export default (dispatch) => (opts, handler) => {
123
+ export default (opts) => (dispatch) => (opts, handler) => {
125
124
  if (!opts.cache || opts.upgrade) {
126
125
  return dispatch(opts, handler)
127
126
  }
@@ -0,0 +1,68 @@
1
+ import assert from 'node:assert'
2
+ import { DecoratorHandler } from '../utils.js'
3
+ import CacheableLookup from 'cacheable-lookup'
4
+
5
+ let DEFAULT_DNS
6
+
7
+ class Handler extends DecoratorHandler {
8
+ #handler
9
+ #opts
10
+ #hostname
11
+
12
+ constructor(opts, hostname, { handler }) {
13
+ super(handler)
14
+
15
+ this.#handler = handler
16
+ this.#opts = opts
17
+ this.#hostname = hostname
18
+ }
19
+
20
+ onError(err) {
21
+ if (
22
+ err.code &&
23
+ [
24
+ 'ECONNRESET',
25
+ 'ECONNREFUSED',
26
+ 'ENOTFOUND',
27
+ 'ENETDOWN',
28
+ 'ENETUNREACH',
29
+ 'EHOSTDOWN',
30
+ 'EHOSTUNREACH',
31
+ 'EPIPE',
32
+ ].includes(err.code)
33
+ ) {
34
+ this.#opts.dns.clear(this.#hostname)
35
+ }
36
+
37
+ return this.#handler.onError(err)
38
+ }
39
+ }
40
+
41
+ export default (opts) => (dispatch) => (opts, handler) => {
42
+ const dns = opts.dns ?? (DEFAULT_DNS ??= new CacheableLookup())
43
+
44
+ if (!dns) {
45
+ dispatch(opts, handler)
46
+ return
47
+ }
48
+
49
+ try {
50
+ assert(typeof dns.lookup === 'function')
51
+ assert(typeof dns.clear === 'function')
52
+
53
+ const url = new URL(opts.origin)
54
+ const hostname = url.hostname
55
+ dns.lookup(hostname, (err, address) => {
56
+ if (err) {
57
+ handler.onConnect(() => {})
58
+ handler.onError(err)
59
+ } else {
60
+ url.hostname = address
61
+ dispatch({ ...opts, origin: url.origin }, new Handler(opts, hostname, { handler }))
62
+ }
63
+ })
64
+ } catch (err) {
65
+ handler.onConnect(() => {})
66
+ handler.onError(err)
67
+ }
68
+ }
@@ -1,6 +1,4 @@
1
- import { performance } from 'node:perf_hooks'
2
- import { parseHeaders } from '../utils.js'
3
- import { DecoratorHandler } from 'undici'
1
+ import { DecoratorHandler, parseHeaders } from '../utils.js'
4
2
 
5
3
  class Handler extends DecoratorHandler {
6
4
  #handler
@@ -114,5 +112,5 @@ class Handler extends DecoratorHandler {
114
112
  }
115
113
  }
116
114
 
117
- export default (dispatch) => (opts, handler) =>
115
+ export default (opts) => (dispatch) => (opts, handler) =>
118
116
  opts.logger ? dispatch(opts, new Handler(opts, { handler })) : dispatch(opts, handler)
@@ -1,6 +1,6 @@
1
1
  import net from 'node:net'
2
2
  import createError from 'http-errors'
3
- import { DecoratorHandler } from 'undici'
3
+ import { DecoratorHandler } from '../utils.js'
4
4
 
5
5
  class Handler extends DecoratorHandler {
6
6
  #handler
@@ -55,11 +55,17 @@ class Handler extends DecoratorHandler {
55
55
  }
56
56
  }
57
57
 
58
- export default (dispatch) => (opts, handler) => {
58
+ export default (opts) => (dispatch) => (opts, handler) => {
59
59
  if (!opts.proxy) {
60
60
  return dispatch(opts, handler)
61
61
  }
62
62
 
63
+ const expectsPayload =
64
+ opts.method === 'PUT' ||
65
+ opts.method === 'POST' ||
66
+ opts.method === 'PATCH' ||
67
+ opts.method === 'QUERY'
68
+
63
69
  const headers = reduceHeaders(
64
70
  {
65
71
  headers: opts.headers ?? {},
@@ -68,7 +74,15 @@ export default (dispatch) => (opts, handler) => {
68
74
  proxyName: opts.proxy.name,
69
75
  },
70
76
  (obj, key, val) => {
71
- obj[key] = val
77
+ if (key === 'content-length' && !expectsPayload) {
78
+ // https://tools.ietf.org/html/rfc7230#section-3.3.2
79
+ // A user agent SHOULD NOT send a Content-Length header field when
80
+ // the request message does not contain a payload body and the method
81
+ // semantics do not anticipate such a body.
82
+ // undici will error if provided an unexpected content-length: 0 header.
83
+ } else {
84
+ obj[key] = val
85
+ }
72
86
  return obj
73
87
  },
74
88
  {},
@@ -1,6 +1,5 @@
1
1
  import assert from 'node:assert'
2
- import { isDisturbed, parseHeaders, parseURL } from '../utils.js'
3
- import { DecoratorHandler } from 'undici'
2
+ import { DecoratorHandler, isDisturbed, parseHeaders, parseURL } from '../utils.js'
4
3
 
5
4
  const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
6
5
 
@@ -185,5 +184,5 @@ function cleanRequestHeaders(headers, removeContent, unknownOrigin) {
185
184
  return ret
186
185
  }
187
186
 
188
- export default (dispatch) => (opts, handler) =>
187
+ export default (opts) => (dispatch) => (opts, handler) =>
189
188
  opts.follow ? dispatch(opts, new Handler(opts, { handler, dispatch })) : dispatch(opts, handler)
@@ -1,27 +1,26 @@
1
- export default (dispatch) => (opts, handler) => {
1
+ export default (opts) => (dispatch) => (opts, handler) => {
2
2
  if (typeof opts.body !== 'function') {
3
3
  return dispatch(opts, handler)
4
4
  }
5
5
 
6
- // TODO (fix): Can we do signal in a better way using
7
- // a handler?
6
+ try {
7
+ const body = opts.body({ signal: opts.signal })
8
8
 
9
- const body = opts.body({ signal: opts.signal })
10
-
11
- if (typeof body?.then === 'function') {
12
- body.then(
13
- (body) => {
14
- // Workaround: https://github.com/nodejs/undici/pull/2497
15
- body.on('error', () => {})
16
-
17
- dispatch({ ...opts, body }, handler)
18
- },
19
- (err) => {
20
- handler.onConnect(() => {})
21
- handler.onError(err)
22
- },
23
- )
24
- } else {
25
- dispatch({ ...opts, body }, handler)
9
+ if (typeof body?.then === 'function') {
10
+ body.then(
11
+ (body) => {
12
+ dispatch({ ...opts, body }, handler)
13
+ },
14
+ (err) => {
15
+ handler.onConnect(() => {})
16
+ handler.onError(err)
17
+ },
18
+ )
19
+ } else {
20
+ dispatch({ ...opts, body }, handler)
21
+ }
22
+ } catch (err) {
23
+ handler.onConnect(() => {})
24
+ handler.onError(err)
26
25
  }
27
26
  }
@@ -11,8 +11,8 @@ function genReqId() {
11
11
  return `req-${nextReqId.toString(36)}`
12
12
  }
13
13
 
14
- export default (dispatch) => (opts, handler) => {
15
- let id = opts.id ?? opts.headers?.['request-id']
14
+ export default (opts) => (dispatch) => (opts, handler) => {
15
+ let id = opts.headers?.['request-id']
16
16
  id = id ? `${id},${genReqId()}` : genReqId()
17
17
 
18
18
  return dispatch(
@@ -1,6 +1,5 @@
1
- import { parseHeaders } from '../utils.js'
2
1
  import createHttpError from 'http-errors'
3
- import { DecoratorHandler } from 'undici'
2
+ import { DecoratorHandler, parseHeaders } from '../utils.js'
4
3
 
5
4
  class Handler extends DecoratorHandler {
6
5
  #handler
@@ -95,7 +94,7 @@ class Handler extends DecoratorHandler {
95
94
  }
96
95
  }
97
96
 
98
- export default (dispatch) => (opts, handler) =>
97
+ export default (opts) => (dispatch) => (opts, handler) =>
99
98
  opts.error !== false && opts.throwOnError !== false
100
99
  ? dispatch(opts, new Handler(opts, { handler }))
101
100
  : dispatch(opts, handler)
@@ -1,6 +1,11 @@
1
1
  import assert from 'node:assert'
2
- import { isDisturbed, parseHeaders, parseRangeHeader, retry as retryFn } from '../utils.js'
3
- import { DecoratorHandler } from 'undici'
2
+ import {
3
+ DecoratorHandler,
4
+ isDisturbed,
5
+ parseHeaders,
6
+ parseRangeHeader,
7
+ retry as retryFn,
8
+ } from '../utils.js'
4
9
 
5
10
  // TODO (fix): What about onUpgrade?
6
11
  class Handler extends DecoratorHandler {
@@ -200,9 +205,8 @@ class Handler extends DecoratorHandler {
200
205
  }
201
206
  }
202
207
 
203
- export default (dispatch) => (opts, handler) => {
208
+ export default (opts) => (dispatch) => (opts, handler) =>
204
209
  // TODO (fix): HEAD, PUT, PATCH, DELETE, OPTIONS?
205
- return opts.retry !== false && opts.method === 'GET' && !opts.upgrade
210
+ opts.retry !== false && opts.method === 'GET' && !opts.upgrade
206
211
  ? dispatch(opts, new Handler(opts, { handler, dispatch }))
207
212
  : dispatch(opts, handler)
208
- }
@@ -1,7 +1,8 @@
1
1
  import crypto from 'node:crypto'
2
2
  import assert from 'node:assert'
3
- import { parseHeaders } from '../utils.js'
4
- import { DecoratorHandler } from 'undici'
3
+ import { DecoratorHandler, parseHeaders } from '../utils.js'
4
+
5
+ const DEFAULT_OPTS = { hash: null }
5
6
 
6
7
  class Handler extends DecoratorHandler {
7
8
  #handler
@@ -17,7 +18,8 @@ class Handler extends DecoratorHandler {
17
18
  super(handler)
18
19
 
19
20
  this.#handler = handler
20
- this.#verifyOpts = opts.verify === true ? { hash: true, size: true } : opts.verify
21
+ this.#verifyOpts =
22
+ opts.verify === true ? { hash: true, size: true } : opts.verify ?? DEFAULT_OPTS
21
23
  }
22
24
 
23
25
  onConnect(abort) {
@@ -80,7 +82,7 @@ class Handler extends DecoratorHandler {
80
82
  }
81
83
  }
82
84
 
83
- export default (dispatch) => (opts, handler) =>
85
+ export default (opts) => (dispatch) => (opts, handler) =>
84
86
  !opts.upgrade && opts.verify !== false
85
87
  ? dispatch(opts, new Handler(opts, { handler }))
86
88
  : dispatch(opts, handler)
package/lib/utils.js CHANGED
@@ -1,16 +1,12 @@
1
1
  import tp from 'node:timers/promises'
2
2
  import cacheControlParser from 'cache-control-parser'
3
3
  import stream from 'node:stream'
4
- import { util } from 'undici'
4
+ import { util } from '@nxtedition/undici'
5
5
 
6
6
  export function parseCacheControl(str) {
7
7
  return str ? cacheControlParser.parse(str) : null
8
8
  }
9
9
 
10
- export function headerNameToString(name) {
11
- return util.headerNameToString(name)
12
- }
13
-
14
10
  // Parsed accordingly to RFC 9110
15
11
  // https://www.rfc-editor.org/rfc/rfc9110#field.content-range
16
12
  export function parseRangeHeader(range) {
@@ -204,84 +200,7 @@ export function parseOrigin(url) {
204
200
  * @returns {Record<string, string | string[]>}
205
201
  */
206
202
  export function parseHeaders(headers, obj) {
207
- if (obj == null) {
208
- obj = {}
209
- } else {
210
- // TODO (fix): assert obj values type?
211
- }
212
-
213
- if (Array.isArray(headers)) {
214
- for (let i = 0; i < headers.length; i += 2) {
215
- const key2 = headers[i]
216
- const val2 = headers[i + 1]
217
-
218
- // TODO (fix): assert key2 type?
219
- // TODO (fix): assert val2 type?
220
-
221
- if (val2 == null) {
222
- continue
223
- }
224
-
225
- const key = headerNameToString(key2)
226
- let val = obj[key]
227
-
228
- if (val) {
229
- if (!Array.isArray(val)) {
230
- val = [val]
231
- obj[key] = val
232
- }
233
-
234
- if (Array.isArray(val2)) {
235
- val.push(...val2.filter((x) => x != null).map((x) => `${x}`))
236
- } else {
237
- val.push(`${val2}`)
238
- }
239
- } else {
240
- obj[key] = Array.isArray(val2)
241
- ? val2.filter((x) => x != null).map((x) => `${x}`)
242
- : `${val2}`
243
- }
244
- }
245
- } else if (typeof headers === 'object' && headers !== null) {
246
- for (const key2 of Object.keys(headers)) {
247
- const val2 = headers[key2]
248
-
249
- // TODO (fix): assert key2 type?
250
- // TODO (fix): assert val2 type?
251
-
252
- if (val2 == null) {
253
- continue
254
- }
255
-
256
- const key = headerNameToString(key2)
257
- let val = obj[key]
258
-
259
- if (val) {
260
- if (!Array.isArray(val)) {
261
- val = [val]
262
- obj[key] = val
263
- }
264
- if (Array.isArray(val2)) {
265
- val.push(...val2.filter((x) => x != null).map((x) => `${x}`))
266
- } else {
267
- val.push(`${val2}`)
268
- }
269
- } else if (val2 != null) {
270
- obj[key] = Array.isArray(val2)
271
- ? val2.filter((x) => x != null).map((x) => `${x}`)
272
- : `${val2}`
273
- }
274
- }
275
- } else if (headers != null) {
276
- throw new Error('invalid argument: headers')
277
- }
278
-
279
- // See https://github.com/nodejs/node/pull/46528
280
- if ('content-length' in obj && 'content-disposition' in obj) {
281
- obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1')
282
- }
283
-
284
- return obj
203
+ return util.parseHeaders(headers, obj)
285
204
  }
286
205
 
287
206
  export class AbortError extends Error {
@@ -328,3 +247,46 @@ export function bodyLength(body) {
328
247
 
329
248
  return null
330
249
  }
250
+
251
+ export class DecoratorHandler {
252
+ #handler
253
+
254
+ constructor(handler) {
255
+ if (typeof handler !== 'object' || handler === null) {
256
+ throw new TypeError('handler must be an object')
257
+ }
258
+ this.#handler = handler
259
+ }
260
+
261
+ onConnect(...args) {
262
+ return this.#handler.onConnect?.(...args)
263
+ }
264
+
265
+ onError(...args) {
266
+ return this.#handler.onError?.(...args)
267
+ }
268
+
269
+ onUpgrade(...args) {
270
+ return this.#handler.onUpgrade?.(...args)
271
+ }
272
+
273
+ onResponseStarted(...args) {
274
+ return this.#handler.onResponseStarted?.(...args)
275
+ }
276
+
277
+ onHeaders(...args) {
278
+ return this.#handler.onHeaders?.(...args)
279
+ }
280
+
281
+ onData(...args) {
282
+ return this.#handler.onData?.(...args)
283
+ }
284
+
285
+ onComplete(...args) {
286
+ return this.#handler.onComplete?.(...args)
287
+ }
288
+
289
+ onBodySent(...args) {
290
+ return this.#handler.onBodySent?.(...args)
291
+ }
292
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "2.2.6",
3
+ "version": "3.0.0",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",
@@ -9,14 +9,14 @@
9
9
  "lib/*"
10
10
  ],
11
11
  "dependencies": {
12
+ "@nxtedition/undici": "^7.0.1",
12
13
  "cache-control-parser": "^2.0.6",
13
14
  "cacheable-lookup": "^7.0.0",
14
15
  "http-errors": "^2.0.0",
15
- "lru-cache": "^10.2.0",
16
- "undici": "^6.18.2"
16
+ "lru-cache": "^10.2.0"
17
17
  },
18
18
  "devDependencies": {
19
- "@types/node": "^20.14.2",
19
+ "@types/node": "^20.14.8",
20
20
  "eslint": "^8.0.0",
21
21
  "eslint-config-prettier": "^9.1.0",
22
22
  "eslint-config-standard": "^17.0.0",
package/lib/readable.js DELETED
@@ -1,382 +0,0 @@
1
- import assert from 'node:assert'
2
- import { Readable } from 'node:stream'
3
- import { errors as undiciErrors } from 'undici'
4
- import { isDisturbed } from './utils.js'
5
-
6
- const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = undiciErrors
7
-
8
- const kConsume = Symbol('kConsume')
9
- const kReading = Symbol('kReading')
10
- const kBody = Symbol('kBody')
11
- const kAbort = Symbol('abort')
12
- const kContentType = Symbol('kContentType')
13
- const kReadLength = Symbol('kReadLength')
14
-
15
- const kStatusCode = Symbol('kStatusCode')
16
- const kStatusMessage = Symbol('kStatusMessage')
17
- const kHeaders = Symbol('kHeaders')
18
- const kSize = Symbol('kSize')
19
- const kHandler = Symbol('kHandler')
20
- const kMethod = Symbol('kMethod')
21
-
22
- const noop = () => {}
23
-
24
- let ABORT_ERROR
25
-
26
- export class BodyReadable extends Readable {
27
- constructor(
28
- handler,
29
- {
30
- contentType = '',
31
- method,
32
- statusCode,
33
- statusMessage,
34
- headers,
35
- size,
36
- abort,
37
- highWaterMark,
38
- resume,
39
- },
40
- ) {
41
- super({
42
- autoDestroy: true,
43
- read: resume,
44
- highWaterMark,
45
- })
46
-
47
- this._readableState.dataEmitted = false
48
-
49
- this[kHandler] = handler
50
- this[kStatusCode] = statusCode
51
- this[kStatusMessage] = statusMessage
52
- this[kHeaders] = headers
53
- this[kSize] = Number.isFinite(size) ? size : null
54
- this[kAbort] = abort
55
- this[kReadLength] = 0
56
-
57
- this[kConsume] = null
58
- this[kBody] = null
59
- this[kContentType] = contentType
60
- this[kMethod] = method
61
-
62
- // Is stream being consumed through Readable API?
63
- // This is an optimization so that we avoid checking
64
- // for 'data' and 'readable' listeners in the hot path
65
- // inside push().
66
- this[kReading] = false
67
- }
68
-
69
- get statusCode() {
70
- return this[kStatusCode]
71
- }
72
-
73
- get statusMessage() {
74
- return this[kStatusMessage]
75
- }
76
-
77
- get headers() {
78
- return this[kHeaders]
79
- }
80
-
81
- get size() {
82
- return this[kSize]
83
- }
84
-
85
- get body() {
86
- return this
87
- }
88
-
89
- _destroy(err, callback) {
90
- if (!err && !this._readableState.endEmitted) {
91
- err = ABORT_ERROR ??= new RequestAbortedError()
92
- }
93
-
94
- if (err) {
95
- this[kAbort]()
96
- }
97
-
98
- if (this[kHandler].signal) {
99
- this[kHandler].signal.removeEventListener('abort', this[kHandler].onAbort)
100
- this[kHandler].signal = null
101
- }
102
-
103
- if (!this[kReading]) {
104
- // Workaround for Node "bug". If the stream is destroyed in same
105
- // tick as it is created, then a user who is waiting for a
106
- // promise (i.e micro tick) for installing a 'error' listener will
107
- // never get a chance and will always encounter an unhandled exception.
108
- // - tick => process.nextTick(fn)
109
- // - micro tick => queueMicrotask(fn)
110
- setImmediate(() => callback(err))
111
- } else {
112
- callback(err)
113
- }
114
- }
115
-
116
- on(ev, ...args) {
117
- if (ev === 'data' || ev === 'readable') {
118
- this[kReading] = true
119
- }
120
- return super.on(ev, ...args)
121
- }
122
-
123
- addListener(ev, ...args) {
124
- return this.on(ev, ...args)
125
- }
126
-
127
- off(ev, ...args) {
128
- const ret = super.off(ev, ...args)
129
- if (ev === 'data' || ev === 'readable') {
130
- this[kReading] = this.listenerCount('data') > 0 || this.listenerCount('readable') > 0
131
- }
132
- return ret
133
- }
134
-
135
- removeListener(ev, ...args) {
136
- return this.off(ev, ...args)
137
- }
138
-
139
- setEncoding(encoding) {
140
- if (encoding) {
141
- throw new Error('not supported')
142
- }
143
- return super.setEncoding(encoding)
144
- }
145
-
146
- push(chunk) {
147
- if (chunk != null) {
148
- this[kReadLength] += chunk.length
149
- }
150
-
151
- if (this[kConsume] && chunk !== null) {
152
- consumePush(this[kConsume], chunk)
153
- return this[kReading] ? super.push(chunk) : true
154
- }
155
-
156
- return super.push(chunk)
157
- }
158
-
159
- // https://fetch.spec.whatwg.org/#dom-body-text
160
- async text() {
161
- return consume(this, 'text')
162
- }
163
-
164
- // https://fetch.spec.whatwg.org/#dom-body-json
165
- async json() {
166
- return consume(this, 'json')
167
- }
168
-
169
- // https://fetch.spec.whatwg.org/#dom-body-blob
170
- async blob() {
171
- return consume(this, 'blob')
172
- }
173
-
174
- // https://fetch.spec.whatwg.org/#dom-body-arraybuffer
175
- async arrayBuffer() {
176
- return consume(this, 'arrayBuffer')
177
- }
178
-
179
- // https://fetch.spec.whatwg.org/#dom-body-formdata
180
- async formData() {
181
- // TODO: Implement.
182
- throw new NotSupportedError()
183
- }
184
-
185
- // https://fetch.spec.whatwg.org/#dom-body-bodyused
186
- get bodyUsed() {
187
- return isDisturbed(this)
188
- }
189
-
190
- async dump(opts) {
191
- let limit = Number.isFinite(opts?.limit) ? opts.limit : 128 * 1024
192
- const signal = opts?.signal
193
-
194
- if (this[kSize] != null && this[kSize] - this[kReadLength] > limit) {
195
- this.destroy(signal.reason ?? new RequestAbortedError())
196
- }
197
-
198
- if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) {
199
- throw new InvalidArgumentError('signal must be an AbortSignal')
200
- }
201
-
202
- signal?.throwIfAborted()
203
-
204
- if (this._readableState.closeEmitted) {
205
- return null
206
- }
207
-
208
- const contentLength = this.headers['content-length']
209
- ? Number(this.headers['content-length'])
210
- : null
211
-
212
- if (this[kMethod] !== 'HEAD' && contentLength != null && contentLength >= limit) {
213
- this.on('error', () => {}).destroy()
214
- return
215
- }
216
-
217
- return await new Promise((resolve, reject) => {
218
- const onAbort = () => {
219
- this.destroy(signal.reason ?? new RequestAbortedError())
220
- }
221
- signal?.addEventListener('abort', onAbort)
222
-
223
- this.on('close', function () {
224
- signal?.removeEventListener('abort', onAbort)
225
- if (signal?.aborted) {
226
- reject(signal.reason ?? new RequestAbortedError())
227
- } else {
228
- resolve(null)
229
- }
230
- })
231
- .on('error', noop)
232
- .on('data', function (chunk) {
233
- limit -= chunk.length
234
- if (limit <= 0) {
235
- this.destroy()
236
- }
237
- })
238
- .resume()
239
- })
240
- }
241
- }
242
-
243
- // https://streams.spec.whatwg.org/#readablestream-locked
244
- function isLocked(self) {
245
- // Consume is an implicit lock.
246
- return (self[kBody] && self[kBody].locked === true) || self[kConsume]
247
- }
248
-
249
- // https://fetch.spec.whatwg.org/#body-unusable
250
- function isUnusable(self) {
251
- return isDisturbed(self) || isLocked(self)
252
- }
253
-
254
- async function consume(stream, type) {
255
- assert(!stream[kConsume])
256
-
257
- return new Promise((resolve, reject) => {
258
- if (isUnusable(stream)) {
259
- const rState = stream._readableState
260
- if (rState.destroyed && rState.closeEmitted === false) {
261
- stream
262
- .on('error', (err) => {
263
- reject(err)
264
- })
265
- .on('close', () => {
266
- reject(new TypeError('unusable'))
267
- })
268
- } else {
269
- reject(rState.errored ?? new TypeError('unusable'))
270
- }
271
- } else {
272
- queueMicrotask(() => {
273
- stream[kConsume] = {
274
- type,
275
- stream,
276
- resolve,
277
- reject,
278
- length: 0,
279
- body: [],
280
- }
281
-
282
- stream
283
- .on('error', function (err) {
284
- consumeFinish(this[kConsume], err)
285
- })
286
- .on('close', function () {
287
- if (this[kConsume].body !== null) {
288
- consumeFinish(this[kConsume], new RequestAbortedError())
289
- }
290
- })
291
-
292
- consumeStart(stream[kConsume])
293
- })
294
- }
295
- })
296
- }
297
-
298
- function consumeStart(consume) {
299
- if (consume.body === null) {
300
- return
301
- }
302
-
303
- const { _readableState: state } = consume.stream
304
-
305
- if (state.bufferIndex) {
306
- const start = state.bufferIndex
307
- const end = state.buffer.length
308
- for (let n = start; n < end; n++) {
309
- consumePush(consume, state.buffer[n])
310
- }
311
- } else {
312
- for (const chunk of state.buffer) {
313
- consumePush(consume, chunk)
314
- }
315
- }
316
-
317
- if (state.endEmitted) {
318
- consumeEnd(this[kConsume])
319
- } else {
320
- consume.stream.on('end', function () {
321
- consumeEnd(this[kConsume])
322
- })
323
- }
324
-
325
- consume.stream.resume()
326
-
327
- while (consume.stream.read() != null) {
328
- // Loop
329
- }
330
- }
331
-
332
- function consumeEnd(consume) {
333
- const { type, body, resolve, stream, length } = consume
334
-
335
- try {
336
- if (type === 'text') {
337
- resolve(Buffer.concat(body).toString().toWellFormed())
338
- } else if (type === 'json') {
339
- resolve(JSON.parse(Buffer.concat(body)))
340
- } else if (type === 'arrayBuffer') {
341
- const dst = new Uint8Array(length)
342
-
343
- let pos = 0
344
- for (const buf of body) {
345
- dst.set(buf, pos)
346
- pos += buf.byteLength
347
- }
348
-
349
- resolve(dst.buffer)
350
- } else if (type === 'blob') {
351
- resolve(new Blob(body, { type: stream[kContentType] }))
352
- }
353
-
354
- consumeFinish(consume)
355
- } catch (err) {
356
- stream.destroy(err)
357
- }
358
- }
359
-
360
- function consumePush(consume, chunk) {
361
- consume.length += chunk.length
362
- consume.body.push(chunk)
363
- }
364
-
365
- function consumeFinish(consume, err) {
366
- if (consume.body === null) {
367
- return
368
- }
369
-
370
- if (err) {
371
- consume.reject(err)
372
- } else {
373
- consume.resolve()
374
- }
375
-
376
- consume.type = null
377
- consume.stream = null
378
- consume.resolve = null
379
- consume.reject = null
380
- consume.length = 0
381
- consume.body = null
382
- }