@nxtedition/nxt-undici 6.0.17 → 6.0.19

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,9 +1,8 @@
1
1
  import net from 'node:net'
2
- import { resolve4 } from 'node:dns'
2
+ import assert from 'node:assert'
3
+ import * as dns from 'node:dns'
3
4
  import { DecoratorHandler, getFastNow } from '../utils.js'
4
5
 
5
- const MAX_TTL = 10e3
6
-
7
6
  class Handler extends DecoratorHandler {
8
7
  #callback
9
8
  #statusCode
@@ -37,33 +36,29 @@ export default () => (dispatch) => {
37
36
  const cache = new Map()
38
37
  const promises = new Map()
39
38
 
40
- function resolve(hostname) {
39
+ function resolve(resolve4, hostname) {
41
40
  let promise = promises.get(hostname)
42
41
  if (!promise) {
43
42
  promise = new Promise((resolve) =>
44
43
  resolve4(hostname, { ttl: true }, (err, records) => {
45
- let ret
46
-
47
44
  if (err) {
48
- ret = { error: err }
45
+ resolve([err, null])
49
46
  } else {
50
47
  const now = getFastNow()
51
- ret = {
52
- value: records.map(({ address, ttl }) => ({
53
- address,
54
- expires: now + Math.min(MAX_TTL, 1e3 * ttl),
55
- pending: 0,
56
- errored: 0,
57
- counter: 0,
58
- })),
59
- }
60
- }
48
+ const val = records.map(({ address, ttl }) => ({
49
+ address,
50
+ expires: now + 1e3 * ttl,
51
+ pending: 0,
52
+ errored: 0,
53
+ counter: 0,
54
+ }))
61
55
 
62
- cache.set(hostname, ret)
56
+ cache.set(hostname, val)
63
57
 
64
- promises.delete(hostname)
58
+ resolve([null, val])
59
+ }
65
60
 
66
- resolve(ret)
61
+ assert(promises.delete(hostname))
67
62
  }),
68
63
  )
69
64
  promises.set(hostname, promise)
@@ -87,16 +82,22 @@ export default () => (dispatch) => {
87
82
 
88
83
  const now = getFastNow()
89
84
 
90
- let records = cache.get(hostname)?.value
85
+ let records = cache.get(hostname)
86
+
87
+ const resolve4 = opts.dns.resolve4 || dns.resolve4
91
88
 
92
89
  if (records == null || records.every((x) => x.expires < now)) {
93
- const { value, error } = await resolve(hostname)
94
- if (error) {
95
- throw error
90
+ const [err, val] = await resolve(resolve4, hostname)
91
+
92
+ if (err) {
93
+ throw err
96
94
  }
97
- records = value
95
+
96
+ assert(val.every((x) => x.expires > 0))
97
+
98
+ records = val
98
99
  } else if (records.some((x) => x.expires < now + 1e3)) {
99
- resolve(hostname)
100
+ resolve(resolve4, hostname)
100
101
  }
101
102
 
102
103
  records.sort(
@@ -1,3 +1,5 @@
1
+ import { once } from 'node:events'
2
+ import http from 'node:http'
1
3
  import { test } from 'tap'
2
4
  import { request } from '../index.js'
3
5
 
@@ -7,3 +9,74 @@ test('retry destroy pre response', async (t) => {
7
9
  t.equal(statusCode, 200)
8
10
  t.end()
9
11
  })
12
+
13
+ test('expire & retry on error', async (t) => {
14
+ t.plan(3)
15
+
16
+ const server = http
17
+ .createServer((req, res) => {
18
+ res.end()
19
+ })
20
+ .listen(0)
21
+ t.teardown(server.close.bind(server))
22
+
23
+ await once(server, 'listening')
24
+
25
+ let counter = 0
26
+ const { body } = await request(`http://asd.com:${server.address().port}`, {
27
+ dns: {
28
+ resolve4(hostname, opts, callback) {
29
+ t.pass()
30
+ if (counter++ === 0) {
31
+ process.nextTick(callback, null, [{ address: '11.9.9.9', ttl: 600 }])
32
+ } else {
33
+ process.nextTick(callback, null, [{ address: '127.0.0.1', ttl: 600 }])
34
+ }
35
+ },
36
+ },
37
+ retry: 2,
38
+ })
39
+ await body.dump()
40
+
41
+ t.pass()
42
+ })
43
+
44
+ test('expire on error', async (t) => {
45
+ t.plan(2)
46
+
47
+ const server = http
48
+ .createServer((req, res) => {
49
+ res.end()
50
+ })
51
+ .listen(0)
52
+ t.teardown(server.close.bind(server))
53
+
54
+ await once(server, 'listening')
55
+
56
+ try {
57
+ const { body } = await request(`http://123.com:${server.address().port}`, {
58
+ dns: {
59
+ resolve4(hostname, opts, callback) {
60
+ process.nextTick(callback, null, [{ address: '10.9.9.9', ttl: 600 }])
61
+ },
62
+ },
63
+ retry: false,
64
+ })
65
+ await body.dump()
66
+ } catch (err) {
67
+ t.equal(err.code, 'UND_ERR_CONNECT_TIMEOUT')
68
+ }
69
+
70
+ const { body } = await request(`http://123.com:${server.address().port}`, {
71
+ dns: {
72
+ resolve4(hostname, opts, callback) {
73
+ process.nextTick(callback, null, [{ address: '127.0.0.1', ttl: 600 }])
74
+ },
75
+ },
76
+ retry: false,
77
+ })
78
+ await body.dump()
79
+
80
+ console.error('### 4')
81
+ t.pass()
82
+ })
@@ -13,6 +13,7 @@ class Handler extends DecoratorHandler {
13
13
  #abort
14
14
  #aborted = false
15
15
  #reason
16
+ #resume
16
17
 
17
18
  #pos
18
19
  #end
@@ -41,6 +42,7 @@ class Handler extends DecoratorHandler {
41
42
  this.#end = null
42
43
  this.#etag = null
43
44
  this.#error = null
45
+ this.#resume = null
44
46
 
45
47
  super.onConnect((reason) => {
46
48
  if (!this.#aborted) {
@@ -62,6 +64,8 @@ class Handler extends DecoratorHandler {
62
64
  }
63
65
 
64
66
  onHeaders(statusCode, headers, resume) {
67
+ this.#resume = resume
68
+
65
69
  if (this.#error == null) {
66
70
  assert(this.#etag == null)
67
71
  assert(this.#pos == null)
@@ -69,18 +73,18 @@ class Handler extends DecoratorHandler {
69
73
  assert(this.#headersSent === false)
70
74
 
71
75
  if (headers.trailer) {
72
- return this.#onHeaders(statusCode, headers, resume)
76
+ return this.#onHeaders(statusCode, headers)
73
77
  }
74
78
 
75
79
  const contentLength = headers['content-length'] ? Number(headers['content-length']) : null
76
80
  if (contentLength != null && !Number.isFinite(contentLength)) {
77
- return this.#onHeaders(statusCode, headers, resume)
81
+ return this.#onHeaders(statusCode, headers)
78
82
  }
79
83
 
80
84
  if (statusCode === 206) {
81
85
  const range = parseRangeHeader(headers['content-range'])
82
86
  if (!range) {
83
- return this.#onHeaders(statusCode, headers, resume)
87
+ return this.#onHeaders(statusCode, headers)
84
88
  }
85
89
 
86
90
  const { start, size, end = size } = range
@@ -100,7 +104,7 @@ class Handler extends DecoratorHandler {
100
104
  this.#end = contentLength
101
105
  this.#etag = headers.etag
102
106
  } else {
103
- return this.#onHeaders(statusCode, headers, resume)
107
+ return this.#onHeaders(statusCode, headers)
104
108
  }
105
109
 
106
110
  // Weak etags are not useful for comparison nor cache
@@ -113,7 +117,7 @@ class Handler extends DecoratorHandler {
113
117
  assert(Number.isFinite(this.#pos))
114
118
  assert(this.#end == null || Number.isFinite(this.#end))
115
119
 
116
- return this.#onHeaders(statusCode, headers, resume)
120
+ return this.#onHeaders(statusCode, headers)
117
121
  } else if (statusCode === 206 || (this.#pos === 0 && statusCode === 200)) {
118
122
  assert(this.#etag != null || !this.#pos)
119
123
 
@@ -207,10 +211,10 @@ class Handler extends DecoratorHandler {
207
211
  super.onError(err)
208
212
  }
209
213
 
210
- #onHeaders(statusCode, headers, resume) {
214
+ #onHeaders(statusCode, headers) {
211
215
  assert(!this.#headersSent)
212
216
  this.#headersSent = true
213
- return super.onHeaders(statusCode, headers, resume)
217
+ return super.onHeaders(statusCode, headers, () => this.#resume())
214
218
  }
215
219
  }
216
220
 
package/lib/request.js CHANGED
@@ -143,7 +143,16 @@ export class RequestHandler {
143
143
  }
144
144
 
145
145
  export function request(dispatch, url, opts) {
146
- if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) {
146
+ if (typeof url === 'object' && url != null && opts == null) {
147
+ opts = url
148
+ url = opts.url ?? opts
149
+ }
150
+
151
+ if (typeof url === 'string') {
152
+ url = new URL(url)
153
+ }
154
+
155
+ if (url == null || typeof url !== 'object') {
147
156
  throw new InvalidArgumentError('invalid url')
148
157
  }
149
158
 
@@ -151,12 +160,6 @@ export function request(dispatch, url, opts) {
151
160
  throw new InvalidArgumentError('invalid opts')
152
161
  }
153
162
 
154
- if (typeof url === 'string') {
155
- url = new URL(url)
156
- } else if (typeof url === 'object' && url != null && opts == null) {
157
- opts = url
158
- }
159
-
160
163
  let origin = url.origin
161
164
  if (!origin) {
162
165
  const protocol = url.protocol ?? 'http:'
package/lib/utils.js CHANGED
@@ -123,6 +123,7 @@ export async function retry(err, retryCount, opts) {
123
123
  'EHOSTDOWN',
124
124
  'EHOSTUNREACH',
125
125
  'EPIPE',
126
+ 'UND_ERR_CONNECT_TIMEOUT',
126
127
  ].includes(err.code)
127
128
  ) {
128
129
  return tp.setTimeout(Math.min(10e3, retryCount * 1e3), undefined, { signal: opts.signal })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "6.0.17",
3
+ "version": "6.0.19",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",