@nxtedition/nxt-undici 3.2.3 → 3.3.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.
package/lib/index.js CHANGED
@@ -20,46 +20,10 @@ export const interceptors = {
20
20
  export { parseHeaders } from './utils.js'
21
21
  export { Client, Pool, Agent, getGlobalDispatcher, setGlobalDispatcher } from '@nxtedition/undici'
22
22
 
23
- export async function request(url, opts) {
24
- // TODO (fix): More argument validation...
25
-
26
- if (typeof url === 'string') {
27
- url = new URL(url)
28
- } else if (url instanceof URL) {
29
- // Do nothing...
30
- } else if (typeof url.origin === 'string' && typeof (url.path ?? url.pathname) === 'string') {
31
- // Do nothing...
32
- }
33
-
34
- if (opts == null && typeof url === 'object' && url != null) {
35
- opts = url
36
- }
37
-
38
- if (url) {
39
- // Do nothing...
40
- } else if (typeof opts.url === 'string') {
41
- url = new URL(opts.url)
42
- } else if (url.url instanceof URL) {
43
- url = opts.url
44
- } else if (typeof opts.origin === 'string' && typeof (opts.path ?? opts.pathname) === 'string') {
45
- url = opts
46
- } else {
47
- throw new Error('missing url')
48
- }
49
-
50
- const method = opts.method ?? (opts.body ? 'POST' : 'GET')
51
- const headers = parseHeaders(opts.headers)
52
-
53
- const userAgent = opts.userAgent ?? globalThis.userAgent
54
- if (userAgent && headers?.['user-agent'] !== userAgent) {
55
- headers['user-agent'] = userAgent
56
- }
57
-
58
- const baseDispatcher = opts.dispatcher ?? undici.getGlobalDispatcher()
59
-
60
- let dispatcher = dispatcherCache.get(baseDispatcher)
61
- if (dispatcher == null) {
62
- dispatcher = baseDispatcher.compose(
23
+ function wrapDispatcher(dispatcher) {
24
+ let wrappedDispatcher = dispatcherCache.get(dispatcher)
25
+ if (wrappedDispatcher == null) {
26
+ wrappedDispatcher = dispatcher.compose(
63
27
  interceptors.responseError(),
64
28
  interceptors.requestBodyFactory(),
65
29
  interceptors.log(),
@@ -71,34 +35,69 @@ export async function request(url, opts) {
71
35
  interceptors.redirect(),
72
36
  interceptors.cache(),
73
37
  interceptors.proxy(),
38
+ (dispatch) => (opts, handler) => {
39
+ const headers = parseHeaders(opts.headers)
40
+
41
+ const userAgent = opts.userAgent ?? globalThis.userAgent
42
+ if (userAgent && headers?.['user-agent'] !== userAgent) {
43
+ headers['user-agent'] = userAgent
44
+ }
45
+
46
+ const url = opts.url ? new URL(opts.url) : null
47
+
48
+ return dispatch(
49
+ {
50
+ id: opts.id,
51
+ origin: opts.origin ?? url?.origin,
52
+ path: opts.path ?? (url?.search ? `${url.pathname}${url.search}` : url?.pathname),
53
+ method: opts.method ?? (opts.body ? 'POST' : 'GET'),
54
+ body: opts.body,
55
+ query: opts.query,
56
+ headers,
57
+ signal: opts.signal,
58
+ reset: opts.reset ?? false,
59
+ blocking: opts.blocking ?? false,
60
+ headersTimeout: opts.headersTimeout,
61
+ bodyTimeout: opts.bodyTimeout,
62
+ idempotent: opts.idempotent,
63
+ retry: opts.retry ?? 8,
64
+ proxy: opts.proxy ?? false,
65
+ cache: opts.cache ?? false,
66
+ upgrade: opts.upgrade ?? false,
67
+ follow: opts.follow ?? 8,
68
+ error: opts.error ?? true,
69
+ verify: opts.verify ?? true,
70
+ logger: opts.logger ?? null,
71
+ lookup: opts.lookup ?? null,
72
+ dns: opts.dns ?? true,
73
+ },
74
+ handler,
75
+ )
76
+ },
74
77
  )
75
- dispatcherCache.set(baseDispatcher, dispatcher)
78
+ dispatcherCache.set(dispatcher, wrappedDispatcher)
79
+ }
80
+ return wrappedDispatcher
81
+ }
82
+
83
+ export function dispatch(dispatcher, opts, handler) {
84
+ return wrapDispatcher(dispatcher).dispatch(opts, handler)
85
+ }
86
+
87
+ export async function request(url, opts) {
88
+ // TODO (fix): More argument validation...
89
+
90
+ if (typeof url === 'string') {
91
+ opts = { url: new URL(url), ...opts }
92
+ } else if (url instanceof URL) {
93
+ opts = { url, ...opts }
94
+ } else if (typeof url.origin === 'string' && typeof (url.path ?? url.pathname) === 'string') {
95
+ opts = { url, ...opts }
96
+ }
97
+
98
+ if (opts == null && typeof url === 'object' && url != null) {
99
+ opts = url
76
100
  }
77
101
 
78
- return await dispatcher.request({
79
- id: opts.id,
80
- origin: url.origin,
81
- path: url.path ?? (url.search ? `${url.pathname}${url.search}` : url.pathname),
82
- method,
83
- dispatcher,
84
- body: opts.body,
85
- query: opts.query,
86
- headers,
87
- signal: opts.signal,
88
- reset: opts.reset ?? false,
89
- blocking: opts.blocking ?? false,
90
- headersTimeout: opts.headersTimeout,
91
- bodyTimeout: opts.bodyTimeout,
92
- idempotent: opts.idempotent,
93
- retry: opts.retry ?? 8,
94
- proxy: opts.proxy ?? false,
95
- cache: opts.cache ?? false,
96
- upgrade: opts.upgrade ?? false,
97
- follow: opts.follow ?? 8,
98
- error: opts.error ?? true,
99
- verify: opts.verify ?? true,
100
- logger: opts.logger ?? null,
101
- lookup: opts.lookup ?? null,
102
- dns: opts.dns ?? null,
103
- })
102
+ return await wrapDispatcher(opts.dispatcher ?? undici.getGlobalDispatcher()).request(opts)
104
103
  }
@@ -127,8 +127,7 @@ export default (opts) => (dispatch) => (opts, handler) => {
127
127
 
128
128
  // TODO (fix): Cache other methods?
129
129
  if (opts.method !== 'GET' && opts.method !== 'HEAD') {
130
- dispatch(opts, handler)
131
- return
130
+ return dispatch(opts, handler)
132
131
  }
133
132
 
134
133
  if (opts.headers?.['cache-control'] || opts.headers?.authorization) {
@@ -141,8 +140,7 @@ export default (opts) => (dispatch) => (opts, handler) => {
141
140
  // cacheControl['min-fresh']
142
141
  // cacheControl['no-transform']
143
142
  // cacheControl['only-if-cached']
144
- dispatch(opts, handler)
145
- return
143
+ return dispatch(opts, handler)
146
144
  }
147
145
 
148
146
  // TODO (fix): Support body...
@@ -1,19 +1,20 @@
1
1
  import assert from 'node:assert'
2
2
  import { DecoratorHandler } from '../utils.js'
3
3
  import CacheableLookup from 'cacheable-lookup'
4
+ import net from 'net'
4
5
 
5
- let DEFAULT_DNS
6
+ const DEFAULT_RESOLVER = new CacheableLookup()
6
7
 
7
8
  class Handler extends DecoratorHandler {
8
9
  #handler
9
- #store
10
+ #resolver
10
11
  #key
11
12
 
12
- constructor({ store, key }, { handler }) {
13
+ constructor({ resolver, key }, { handler }) {
13
14
  super(handler)
14
15
 
15
16
  this.#handler = handler
16
- this.#store = store
17
+ this.#resolver = resolver
17
18
  this.#key = key
18
19
  }
19
20
 
@@ -28,53 +29,66 @@ class Handler extends DecoratorHandler {
28
29
  'ENETUNREACH',
29
30
  'EHOSTDOWN',
30
31
  'EHOSTUNREACH',
32
+ 'ENODATA',
31
33
  'EPIPE',
32
34
  ].includes(err.code)
33
35
  ) {
34
- this.#store.clear(this.#key)
36
+ this.#resolver.clear(this.#key)
35
37
  }
36
38
 
37
39
  return this.#handler.onError(err)
38
40
  }
39
41
  }
40
42
 
41
- export default (opts) => (dispatch) => (opts, handler) => {
42
- const dns = opts.dns ?? (DEFAULT_DNS ??= new CacheableLookup())
43
+ export default (interceptorOpts) => (dispatch) => (opts, handler) => {
44
+ const dns = opts.dns
43
45
 
44
46
  if (!dns) {
45
- dispatch(opts, handler)
46
- return
47
+ return dispatch(opts, handler)
47
48
  }
48
49
 
49
- try {
50
- assert(typeof dns.lookup === 'function')
51
- assert(typeof dns.clear === 'function')
50
+ const {
51
+ resolver = interceptorOpts?.resolver ?? DEFAULT_RESOLVER,
52
+ family = interceptorOpts?.family,
53
+ hints = interceptorOpts?.hints,
54
+ order = interceptorOpts?.order ?? 'ipv4first',
55
+ all = interceptorOpts?.all ?? true,
56
+ } = dns
57
+
58
+ assert(typeof resolver.lookup === 'function')
52
59
 
53
- const { hostname } = new URL(opts.origin)
60
+ const { hostname } = new URL(opts.origin)
54
61
 
55
- const callback = (err, entries) => {
62
+ if (net.isIP(hostname)) {
63
+ dispatch(opts, handler)
64
+ } else {
65
+ const callback = (err, val) => {
56
66
  if (err) {
57
67
  handler.onConnect(() => {})
58
68
  handler.onError(err)
59
69
  } else {
60
70
  const url = new URL(opts.origin)
61
- url.hostname = entries[Math.floor(entries.length * Math.random())].address
71
+ url.hostname = Array.isArray(val)
72
+ ? val[Math.floor(val.length * Math.random())].address
73
+ : val?.address ?? val
62
74
  dispatch(
63
75
  { ...opts, origin: url.origin },
64
- new Handler({ store: dns, key: hostname }, { handler }),
76
+ resolver.clear ? new Handler({ resolver, key: hostname }, { handler }) : handler,
65
77
  )
66
78
  }
67
79
  }
68
80
 
69
- const thenable = dns.lookup(hostname, { all: true }, callback)
70
- if (typeof thenable?.then === 'function') {
71
- thenable.then(
72
- (val) => callback(null, val),
73
- (err) => callback(err),
74
- )
81
+ try {
82
+ const thenable = resolver.lookup(hostname, { family, hints, order, all }, callback)
83
+ if (typeof thenable?.then === 'function') {
84
+ thenable.then(
85
+ (val) => callback(null, val),
86
+ (err) => callback(err),
87
+ )
88
+ }
89
+ } catch (err) {
90
+ handler.onConnect(() => {})
91
+ handler.onError(err)
75
92
  }
76
- } catch (err) {
77
- handler.onConnect(() => {})
78
- handler.onError(err)
79
93
  }
80
94
  }
@@ -2,8 +2,7 @@ export default (opts) => (dispatch) => (opts, handler) => {
2
2
  const lookup = opts.lookup
3
3
 
4
4
  if (!lookup) {
5
- dispatch(opts, handler)
6
- return
5
+ return dispatch(opts, handler)
7
6
  }
8
7
 
9
8
  const callback = (err, origin) => {
@@ -24,6 +23,6 @@ export default (opts) => (dispatch) => (opts, handler) => {
24
23
  )
25
24
  }
26
25
  } catch (err) {
27
- queueMicrotask(() => callback(err))
26
+ callback(err)
28
27
  }
29
28
  }
@@ -55,45 +55,6 @@ class Handler extends DecoratorHandler {
55
55
  }
56
56
  }
57
57
 
58
- export default (opts) => (dispatch) => (opts, handler) => {
59
- if (!opts.proxy) {
60
- return dispatch(opts, handler)
61
- }
62
-
63
- const expectsPayload =
64
- opts.method === 'PUT' ||
65
- opts.method === 'POST' ||
66
- opts.method === 'PATCH' ||
67
- opts.method === 'QUERY'
68
-
69
- const headers = reduceHeaders(
70
- {
71
- headers: opts.headers ?? {},
72
- httpVersion: opts.proxy.httpVersion ?? opts.proxy.req?.httpVersion,
73
- socket: opts.proxy.socket ?? opts.proxy.req?.socket,
74
- proxyName: opts.proxy.name,
75
- },
76
- (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
- }
84
- if (key === 'expect') {
85
- // undici doesn't support expect header.
86
- } else {
87
- obj[key] = val
88
- }
89
- return obj
90
- },
91
- {},
92
- )
93
-
94
- return dispatch({ ...opts, headers }, new Handler(opts.proxy, { handler }))
95
- }
96
-
97
58
  // This expression matches hop-by-hop headers.
98
59
  // These headers are meaningful only for a single transport-level connection,
99
60
  // and must not be retransmitted by proxies or cached.
@@ -201,3 +162,42 @@ function printIp(address, port) {
201
162
  }
202
163
  return str
203
164
  }
165
+
166
+ export default (opts) => (dispatch) => (opts, handler) => {
167
+ if (!opts.proxy) {
168
+ return dispatch(opts, handler)
169
+ }
170
+
171
+ const expectsPayload =
172
+ opts.method === 'PUT' ||
173
+ opts.method === 'POST' ||
174
+ opts.method === 'PATCH' ||
175
+ opts.method === 'QUERY'
176
+
177
+ const headers = reduceHeaders(
178
+ {
179
+ headers: opts.headers ?? {},
180
+ httpVersion: opts.proxy.httpVersion ?? opts.proxy.req?.httpVersion,
181
+ socket: opts.proxy.socket ?? opts.proxy.req?.socket,
182
+ proxyName: opts.proxy.name,
183
+ },
184
+ (obj, key, val) => {
185
+ if (key === 'content-length' && !expectsPayload) {
186
+ // https://tools.ietf.org/html/rfc7230#section-3.3.2
187
+ // A user agent SHOULD NOT send a Content-Length header field when
188
+ // the request message does not contain a payload body and the method
189
+ // semantics do not anticipate such a body.
190
+ // undici will error if provided an unexpected content-length: 0 header.
191
+ }
192
+ if (key === 'expect') {
193
+ // undici doesn't support expect header.
194
+ } else {
195
+ obj[key] = val
196
+ }
197
+ return obj
198
+ },
199
+ {},
200
+ )
201
+
202
+ return dispatch({ ...opts, headers }, new Handler(opts.proxy, { handler }))
203
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "3.2.3",
3
+ "version": "3.3.1",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",