@nxtedition/nxt-undici 1.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.
@@ -0,0 +1,103 @@
1
+ const { parseHeaders, isDisturbed, retry: retryFn } = require('../utils')
2
+ const createError = require('http-errors')
3
+
4
+ class Handler {
5
+ constructor(opts, { dispatch, handler }) {
6
+ this.dispatch = dispatch
7
+ this.handler = handler
8
+ this.opts = opts
9
+ this.abort = null
10
+ this.aborted = false
11
+ this.reason = null
12
+
13
+ this.timeout = null
14
+ this.count = 0
15
+ this.retryPromise = null
16
+
17
+ this.handler.onConnect((reason) => {
18
+ this.aborted = true
19
+ if (this.abort) {
20
+ this.abort(reason)
21
+ } else {
22
+ this.reason = reason
23
+ }
24
+ })
25
+ }
26
+
27
+ onConnect(abort) {
28
+ if (this.aborted) {
29
+ abort(this.reason)
30
+ } else {
31
+ this.abort = abort
32
+ }
33
+ }
34
+
35
+ onUpgrade(statusCode, rawHeaders, socket) {
36
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket)
37
+ }
38
+
39
+ onBodySent(chunk) {
40
+ return this.handler.onBodySent(chunk)
41
+ }
42
+
43
+ onHeaders(statusCode, rawHeaders, resume, statusMessage) {
44
+ if (statusCode < 400) {
45
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
46
+ }
47
+
48
+ const err = createError(statusCode, { headers: parseHeaders(rawHeaders) })
49
+
50
+ const retryPromise = retryFn(err, this.count++, this.opts)
51
+ if (retryPromise == null) {
52
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
53
+ }
54
+
55
+ retryPromise.catch(() => {})
56
+
57
+ this.retryPromise = retryPromise
58
+
59
+ this.abort(err)
60
+
61
+ return false
62
+ }
63
+
64
+ onData(chunk) {
65
+ return this.handler.onData(chunk)
66
+ }
67
+
68
+ onComplete(rawTrailers) {
69
+ return this.handler.onComplete(rawTrailers)
70
+ }
71
+
72
+ onError(err) {
73
+ if (this.timeout) {
74
+ clearTimeout(this.timeout)
75
+ this.timeout = null
76
+ }
77
+
78
+ if (this.retryPromise == null || this.aborted || isDisturbed(this.opts.body)) {
79
+ return this.handler.onError(err)
80
+ }
81
+
82
+ this.retryPromise
83
+ .then(() => {
84
+ this.timeout = null
85
+ try {
86
+ this.dispatch(this.opts, this)
87
+ } catch (err) {
88
+ this.handler.onError(err)
89
+ }
90
+ })
91
+ .catch((err) => {
92
+ this.handler.onError(err)
93
+ })
94
+ this.retryPromise = null
95
+
96
+ this.opts.loggerdebug('retrying response status')
97
+ }
98
+ }
99
+
100
+ module.exports = (dispatch) => (opts, handler) =>
101
+ opts.idempotent && opts.retry
102
+ ? dispatch(opts, new Handler(opts, { handler, dispatch }))
103
+ : dispatch(opts, handler)
@@ -0,0 +1,48 @@
1
+ class Handler {
2
+ constructor(opts, { handler }) {
3
+ this.handler = handler
4
+ this.signal = opts.signal
5
+ this.abort = null
6
+ }
7
+
8
+ onConnect(abort) {
9
+ this.abort = () => abort(this.signal.reason)
10
+
11
+ this.signal.addEventListener('abort', this.abort)
12
+
13
+ if (this.signal.aborted) {
14
+ abort(this.signal.reason)
15
+ } else {
16
+ this.handler.onConnect(abort)
17
+ }
18
+ }
19
+
20
+ onUpgrade(statusCode, rawHeaders, socket) {
21
+ return this.handler.onUpgrade(statusCode, rawHeaders, socket)
22
+ }
23
+
24
+ onBodySent(chunk) {
25
+ return this.handler.onBodySent(chunk)
26
+ }
27
+
28
+ onHeaders(statusCode, rawHeaders, resume, statusMessage) {
29
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
30
+ }
31
+
32
+ onData(chunk) {
33
+ return this.handler.onData(chunk)
34
+ }
35
+
36
+ onComplete(rawTrailers) {
37
+ this.signal.removeEventListener('abort', this.abort)
38
+ return this.handler.onComplete(rawTrailers)
39
+ }
40
+
41
+ onError(err) {
42
+ this.signal.removeEventListener('abort', this.abort)
43
+ return this.handler.onError(err)
44
+ }
45
+ }
46
+
47
+ module.exports = (dispatch) => (opts, handler) =>
48
+ opts.signal ? dispatch(opts, new Handler(opts, { handler })) : dispatch(opts, handler)
package/lib/utils.js ADDED
@@ -0,0 +1,217 @@
1
+ const tp = require('node:timers/promises')
2
+
3
+ function isDisturbed(body) {
4
+ if (body == null || typeof body === 'string' || Buffer.isBuffer(body)) {
5
+ return false
6
+ }
7
+
8
+ if (body.readableDidRead === false) {
9
+ return false
10
+ }
11
+
12
+ return true
13
+ }
14
+
15
+ function parseContentRange(range) {
16
+ if (typeof range !== 'string') {
17
+ return null
18
+ }
19
+
20
+ const m = range.match(/^bytes (\d+)-(\d+)?\/(\d+|\*)$/)
21
+ if (!m) {
22
+ return null
23
+ }
24
+
25
+ const start = m[1] == null ? null : Number(m[1])
26
+ if (!Number.isFinite(start)) {
27
+ return null
28
+ }
29
+
30
+ const end = m[2] == null ? null : Number(m[2])
31
+ if (end !== null && !Number.isFinite(end)) {
32
+ return null
33
+ }
34
+
35
+ const size = m[2] === '*' ? null : Number(m[2])
36
+ if (size !== null && !Number.isFinite(size)) {
37
+ return null
38
+ }
39
+
40
+ return { start, end: end ? end + 1 : size, size }
41
+ }
42
+
43
+ function findHeader(rawHeaders, name) {
44
+ const len = name.length
45
+
46
+ for (let i = 0; i < rawHeaders.length; i += 2) {
47
+ const key = rawHeaders[i + 0]
48
+ if (key.length === len && key.toString().toLowerCase() === name) {
49
+ return rawHeaders[i + 1].toString()
50
+ }
51
+ }
52
+ return null
53
+ }
54
+
55
+ function retry(err, retryCount, opts) {
56
+ if (opts.retry === null || opts.retry === false) {
57
+ return null
58
+ }
59
+
60
+ if (typeof opts.retry === 'function') {
61
+ return opts.retry(err, retryCount, opts)
62
+ }
63
+
64
+ const retryMax = opts.retry?.count ?? opts.maxRetries ?? 8
65
+
66
+ if (retryCount > retryMax) {
67
+ return null
68
+ }
69
+
70
+ if (err.statusCode && [420, 429, 502, 503, 504].includes(err.statusCode)) {
71
+ let retryAfter = err.headers['retry-after'] ? err.headers['retry-after'] * 1e3 : null
72
+ retryAfter = Number.isFinite(retryAfter) ? retryAfter : Math.min(10e3, retryCount * 1e3)
73
+ if (retryAfter != null && Number.isFinite(retryAfter)) {
74
+ return tp.setTimeout(retryAfter, undefined, { signal: opts.signal })
75
+ } else {
76
+ return null
77
+ }
78
+ }
79
+
80
+ if (
81
+ err.code &&
82
+ [
83
+ 'ECONNRESET',
84
+ 'ECONNREFUSED',
85
+ 'ENOTFOUND',
86
+ 'ENETDOWN',
87
+ 'ENETUNREACH',
88
+ 'EHOSTDOWN',
89
+ 'EHOSTUNREACH',
90
+ 'EPIPE',
91
+ ].includes(err.code)
92
+ ) {
93
+ return tp.setTimeout(Math.min(10e3, retryCount * 1e3), undefined, { signal: opts.signal })
94
+ }
95
+
96
+ if (err.message && ['other side closed'].includes(err.message)) {
97
+ return tp.setTimeout(Math.min(10e3, retryCount * 1e3), undefined, { signal: opts.signal })
98
+ }
99
+
100
+ return null
101
+ }
102
+
103
+ function parseURL(url) {
104
+ if (typeof url === 'string') {
105
+ url = new URL(url)
106
+
107
+ if (!/^https?:/.test(url.origin || url.protocol)) {
108
+ throw new Error('Invalid URL protocol: the URL must start with `http:` or `https:`.')
109
+ }
110
+
111
+ return url
112
+ }
113
+
114
+ if (!url || typeof url !== 'object') {
115
+ throw new Error('Invalid URL: The URL argument must be a non-null object.')
116
+ }
117
+
118
+ if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
119
+ throw new Error(
120
+ 'Invalid URL: port must be a valid integer or a string representation of an integer.',
121
+ )
122
+ }
123
+
124
+ if (url.path != null && typeof url.path !== 'string') {
125
+ throw new Error('Invalid URL path: the path must be a string or null/undefined.')
126
+ }
127
+
128
+ if (url.pathname != null && typeof url.pathname !== 'string') {
129
+ throw new Error('Invalid URL pathname: the pathname must be a string or null/undefined.')
130
+ }
131
+
132
+ if (url.hostname != null && typeof url.hostname !== 'string') {
133
+ throw new Error('Invalid URL hostname: the hostname must be a string or null/undefined.')
134
+ }
135
+
136
+ if (url.origin != null && typeof url.origin !== 'string') {
137
+ throw new Error('Invalid URL origin: the origin must be a string or null/undefined.')
138
+ }
139
+
140
+ if (!/^https?:/.test(url.origin || url.protocol)) {
141
+ throw new Error('Invalid URL protocol: the URL must start with `http:` or `https:`.')
142
+ }
143
+
144
+ if (!(url instanceof URL)) {
145
+ const port = url.port != null ? url.port : url.protocol === 'https:' ? 443 : 80
146
+ let origin = url.origin != null ? url.origin : `${url.protocol}//${url.hostname}:${port}`
147
+ let path = url.path != null ? url.path : `${url.pathname || ''}${url.search || ''}`
148
+
149
+ if (origin.endsWith('/')) {
150
+ origin = origin.substring(0, origin.length - 1)
151
+ }
152
+
153
+ if (path && !path.startsWith('/')) {
154
+ path = `/${path}`
155
+ }
156
+ // new URL(path, origin) is unsafe when `path` contains an absolute URL
157
+ // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
158
+ // If first parameter is a relative URL, second param is required, and will be used as the base URL.
159
+ // If first parameter is an absolute URL, a given second param will be ignored.
160
+ url = new URL(origin + path)
161
+ }
162
+
163
+ return url
164
+ }
165
+
166
+ function parseOrigin(url) {
167
+ url = module.exports.parseURL(url)
168
+
169
+ if (url.pathname !== '/' || url.search || url.hash) {
170
+ throw new Error('invalid url')
171
+ }
172
+
173
+ return url
174
+ }
175
+
176
+ function parseHeaders(rawHeaders, obj = {}) {
177
+ for (let i = 0; i < rawHeaders.length; i += 2) {
178
+ const key = rawHeaders[i].toString().toLowerCase()
179
+ let val = obj[key]
180
+ if (!val) {
181
+ obj[key] = rawHeaders[i + 1].toString()
182
+ } else {
183
+ if (!Array.isArray(val)) {
184
+ val = [val]
185
+ obj[key] = val
186
+ }
187
+ val.push(rawHeaders[i + 1].toString())
188
+ }
189
+ }
190
+ return obj
191
+ }
192
+
193
+ class AbortError extends Error {
194
+ constructor(message) {
195
+ super(message ?? 'The operation was aborted')
196
+ this.code = 'ABORT_ERR'
197
+ this.name = 'AbortError'
198
+ }
199
+ }
200
+
201
+ function isStream(obj) {
202
+ return (
203
+ obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
204
+ )
205
+ }
206
+
207
+ module.exports = {
208
+ isStream,
209
+ AbortError,
210
+ parseHeaders,
211
+ isDisturbed,
212
+ parseContentRange,
213
+ findHeader,
214
+ retry,
215
+ parseURL,
216
+ parseOrigin,
217
+ }
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@nxtedition/nxt-undici",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "author": "Robert Nagy <robert.nagy@boffins.se>",
6
+ "main": "lib/index.js",
7
+ "files": [
8
+ "lib/*"
9
+ ],
10
+ "dependencies": {
11
+ "cache-control-parser": "^2.0.4",
12
+ "http-errors": "^2.0.0",
13
+ "lru-cache": "^10.0.1",
14
+ "undici": "^5.26.4",
15
+ "xuid": "^4.1.2"
16
+ }
17
+ }