@nxtedition/lib 14.3.0 → 15.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/app.js +24 -23
- package/errors.js +7 -7
- package/package.json +13 -10
- package/undici/index.js +192 -0
- package/undici/interceptor/abort.js +61 -0
- package/undici/interceptor/catch.js +55 -0
- package/undici/interceptor/content.js +137 -0
- package/undici/interceptor/log.js +57 -0
- package/{proxy.js → undici/interceptor/proxy.js} +97 -23
- package/undici/interceptor/redirect.js +179 -0
- package/undici/interceptor/response-body-dump.js +46 -0
- package/undici/interceptor/response-body-retry.js +155 -0
- package/undici/interceptor/response-retry.js +84 -0
- package/undici/interceptor/response-status-retry.js +96 -0
- package/undici/interceptor/signal.js +47 -0
- package/undici/utils.js +171 -0
- package/undici.js +0 -160
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const assert = require('node:assert')
|
|
2
|
+
const {
|
|
3
|
+
parseContentRange,
|
|
4
|
+
isDisturbed,
|
|
5
|
+
findHeader,
|
|
6
|
+
retryAfter: retryAfterFn,
|
|
7
|
+
} = require('../utils.js')
|
|
8
|
+
|
|
9
|
+
class Handler {
|
|
10
|
+
constructor(opts, { dispatch, handler }) {
|
|
11
|
+
this.dispatch = dispatch
|
|
12
|
+
this.handler = handler
|
|
13
|
+
this.opts = opts
|
|
14
|
+
this.abort = null
|
|
15
|
+
this.aborted = false
|
|
16
|
+
this.reason = null
|
|
17
|
+
|
|
18
|
+
this.count = 0
|
|
19
|
+
this.pos = 0
|
|
20
|
+
this.end = null
|
|
21
|
+
this.error = null
|
|
22
|
+
this.etag = null
|
|
23
|
+
|
|
24
|
+
this.handler.onConnect((reason) => {
|
|
25
|
+
this.aborted = true
|
|
26
|
+
if (this.abort) {
|
|
27
|
+
this.abort(reason)
|
|
28
|
+
} else {
|
|
29
|
+
this.reason = reason
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onConnect(abort) {
|
|
35
|
+
if (this.aborted) {
|
|
36
|
+
abort(this.reason)
|
|
37
|
+
} else {
|
|
38
|
+
this.abort = abort
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onBodySent(chunk) {
|
|
43
|
+
return this.handler.onBodySent(chunk)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
47
|
+
const etag = findHeader(rawHeaders, 'etag')
|
|
48
|
+
|
|
49
|
+
if (this.resume) {
|
|
50
|
+
this.resume = null
|
|
51
|
+
|
|
52
|
+
// TODO (fix): Support other statusCode with skip?
|
|
53
|
+
if (statusCode !== 206) {
|
|
54
|
+
throw this.error
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// TODO (fix): strict vs weak etag?
|
|
58
|
+
if (this.etag == null || this.etag !== etag) {
|
|
59
|
+
throw this.error
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
|
|
63
|
+
if (!contentRange) {
|
|
64
|
+
throw this.error
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { start, size, end = size } = contentRange
|
|
68
|
+
|
|
69
|
+
assert(this.pos === start, 'content-range mismatch')
|
|
70
|
+
assert(this.end == null || this.end === end, 'content-range mismatch')
|
|
71
|
+
|
|
72
|
+
this.resume = resume
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.end == null) {
|
|
77
|
+
if (statusCode === 206) {
|
|
78
|
+
const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
|
|
79
|
+
if (!contentRange) {
|
|
80
|
+
return this.handler.onHeaders(statusCode, rawHeaders, () => this.resume(), statusMessage)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { start, size, end = size } = contentRange
|
|
84
|
+
|
|
85
|
+
this.end = end
|
|
86
|
+
this.pos = Number(start)
|
|
87
|
+
} else {
|
|
88
|
+
const contentLength = findHeader(rawHeaders, 'content-length')
|
|
89
|
+
if (contentLength) {
|
|
90
|
+
this.end = Number(contentLength)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
assert(Number.isFinite(this.pos))
|
|
95
|
+
assert(this.end == null || Number.isFinite(this.end), 'invalid content-length')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.etag = etag
|
|
99
|
+
this.resume = resume
|
|
100
|
+
return this.handler.onHeaders(statusCode, rawHeaders, () => this.resume(), statusMessage)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onData(chunk) {
|
|
104
|
+
this.pos += chunk.length
|
|
105
|
+
this.count = 0
|
|
106
|
+
return this.handler.onData(chunk)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onComplete(rawTrailers) {
|
|
110
|
+
return this.handler.onComplete(rawTrailers)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onError(err) {
|
|
114
|
+
if (this.timeout) {
|
|
115
|
+
clearTimeout(this.timeout)
|
|
116
|
+
this.timeout = null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!this.resume || this.aborted || !this.etag || isDisturbed(this.opts.body)) {
|
|
120
|
+
return this.handler.onError(err)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const retryAfter = retryAfterFn(err, this.count++, this.opts)
|
|
124
|
+
if (retryAfter == null) {
|
|
125
|
+
return this.handler.onError(err)
|
|
126
|
+
}
|
|
127
|
+
assert(Number.isFinite(retryAfter), 'invalid retry')
|
|
128
|
+
|
|
129
|
+
this.error = err
|
|
130
|
+
this.opts = {
|
|
131
|
+
...this.opts,
|
|
132
|
+
headers: {
|
|
133
|
+
...this.opts.headers,
|
|
134
|
+
range: `bytes=${this.pos}-${this.end ?? ''}`,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.opts.logger?.debug('retrying response body', { retryAfter })
|
|
139
|
+
|
|
140
|
+
this.timeout = setTimeout(() => {
|
|
141
|
+
this.timeout = null
|
|
142
|
+
try {
|
|
143
|
+
this.dispatch(this.opts, this)
|
|
144
|
+
} catch (err) {
|
|
145
|
+
this.handler.onError(err)
|
|
146
|
+
}
|
|
147
|
+
}, retryAfter)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = (dispatch) => (opts, handler) => {
|
|
152
|
+
return opts.idempotent && opts.retry && opts.method === 'GET'
|
|
153
|
+
? dispatch(opts, new Handler(opts, { handler, dispatch }))
|
|
154
|
+
: dispatch(opts, handler)
|
|
155
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const assert = require('node:assert')
|
|
2
|
+
const { isDisturbed, retryAfter: retryAfterFn } = require('../utils.js')
|
|
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
|
+
this.timeout = null
|
|
13
|
+
this.count = 0
|
|
14
|
+
this.retryAfter = null
|
|
15
|
+
|
|
16
|
+
this.handler.onConnect((reason) => {
|
|
17
|
+
this.aborted = true
|
|
18
|
+
if (this.abort) {
|
|
19
|
+
this.abort(reason)
|
|
20
|
+
} else {
|
|
21
|
+
this.reason = reason
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onConnect(abort) {
|
|
27
|
+
if (this.aborted) {
|
|
28
|
+
abort(this.reason)
|
|
29
|
+
} else {
|
|
30
|
+
this.abort = abort
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onBodySent(chunk) {
|
|
35
|
+
return this.handler.onBodySent(chunk)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
39
|
+
this.aborted = true
|
|
40
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onData(chunk) {
|
|
44
|
+
return this.handler.onData(chunk)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onComplete(rawTrailers) {
|
|
48
|
+
return this.handler.onComplete(rawTrailers)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
onError(err) {
|
|
52
|
+
if (this.timeout) {
|
|
53
|
+
clearTimeout(this.timeout)
|
|
54
|
+
this.timeout = null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.aborted || isDisturbed(this.opts.body)) {
|
|
58
|
+
return this.handler.onError(err)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const retryAfter = retryAfterFn(err, this.count++, this.opts)
|
|
62
|
+
if (retryAfter == null) {
|
|
63
|
+
return this.handler.onError(err)
|
|
64
|
+
}
|
|
65
|
+
assert(Number.isFinite(retryAfter), 'invalid retryAfter')
|
|
66
|
+
|
|
67
|
+
this.opts.logger?.debug('retrying response', { retryAfter })
|
|
68
|
+
|
|
69
|
+
this.timeout = setTimeout(() => {
|
|
70
|
+
this.timeout = null
|
|
71
|
+
try {
|
|
72
|
+
this.dispatch(this.opts, this)
|
|
73
|
+
} catch (err2) {
|
|
74
|
+
this.handler.onError(err)
|
|
75
|
+
}
|
|
76
|
+
}, retryAfter)
|
|
77
|
+
this.retryAfter = null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = (dispatch) => (opts, handler) =>
|
|
82
|
+
opts.idempotent && opts.retry
|
|
83
|
+
? dispatch(opts, new Handler(opts, { handler, dispatch }))
|
|
84
|
+
: dispatch(opts, handler)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const assert = require('node:assert')
|
|
2
|
+
const { isDisturbed, retryAfter: retryAfterFn } = require('../utils')
|
|
3
|
+
const createError = require('http-errors')
|
|
4
|
+
const { parseHeaders } = require('../../http')
|
|
5
|
+
|
|
6
|
+
class Handler {
|
|
7
|
+
constructor(opts, { dispatch, handler }) {
|
|
8
|
+
this.dispatch = dispatch
|
|
9
|
+
this.handler = handler
|
|
10
|
+
this.opts = opts
|
|
11
|
+
this.abort = null
|
|
12
|
+
this.aborted = false
|
|
13
|
+
this.reason = null
|
|
14
|
+
|
|
15
|
+
this.timeout = null
|
|
16
|
+
this.count = 0
|
|
17
|
+
this.retryAfter = null
|
|
18
|
+
|
|
19
|
+
this.handler.onConnect((reason) => {
|
|
20
|
+
this.aborted = true
|
|
21
|
+
if (this.abort) {
|
|
22
|
+
this.abort(reason)
|
|
23
|
+
} else {
|
|
24
|
+
this.reason = reason
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onConnect(abort) {
|
|
30
|
+
if (this.aborted) {
|
|
31
|
+
abort(this.reason)
|
|
32
|
+
} else {
|
|
33
|
+
this.abort = abort
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onBodySent(chunk) {
|
|
38
|
+
return this.handler.onBodySent(chunk)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
42
|
+
if (statusCode < 400) {
|
|
43
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const err = createError(statusCode, { headers: parseHeaders(rawHeaders) })
|
|
47
|
+
|
|
48
|
+
const retryAfter = retryAfterFn(err, this.count++, this.opts)
|
|
49
|
+
if (retryAfter == null) {
|
|
50
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
51
|
+
}
|
|
52
|
+
assert(Number.isFinite(retryAfter), 'invalid retryAfter')
|
|
53
|
+
|
|
54
|
+
this.retryAfter = retryAfter
|
|
55
|
+
|
|
56
|
+
this.abort(err)
|
|
57
|
+
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onData(chunk) {
|
|
62
|
+
return this.handler.onData(chunk)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onComplete(rawTrailers) {
|
|
66
|
+
return this.handler.onComplete(rawTrailers)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onError(err) {
|
|
70
|
+
if (this.timeout) {
|
|
71
|
+
clearTimeout(this.timeout)
|
|
72
|
+
this.timeout = null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (this.retryAfter == null || this.aborted || isDisturbed(this.opts.body)) {
|
|
76
|
+
return this.handler.onError(err)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.opts.logger?.debug('retrying response status', { retryAfter: this.retryAfter })
|
|
80
|
+
|
|
81
|
+
this.timeout = setTimeout(() => {
|
|
82
|
+
this.timeout = null
|
|
83
|
+
try {
|
|
84
|
+
this.dispatch(this.opts, this)
|
|
85
|
+
} catch (err) {
|
|
86
|
+
this.handler.onError(err)
|
|
87
|
+
}
|
|
88
|
+
}, this.retryAfter)
|
|
89
|
+
this.retryAfter = null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = (dispatch) => (opts, handler) =>
|
|
94
|
+
opts.idempotent && opts.retry
|
|
95
|
+
? dispatch(opts, new Handler(opts, { handler, dispatch }))
|
|
96
|
+
: dispatch(opts, handler)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class Handler {
|
|
2
|
+
constructor(opts, signal, { handler }) {
|
|
3
|
+
this.handler = handler
|
|
4
|
+
this.signal = signal
|
|
5
|
+
this.abort = null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
onConnect(abort) {
|
|
9
|
+
this.abort = () => {
|
|
10
|
+
abort(this.signal.reason)
|
|
11
|
+
}
|
|
12
|
+
this.signal.addEventListener('abort', this.abort)
|
|
13
|
+
|
|
14
|
+
if (this.signal.aborted) {
|
|
15
|
+
abort(this.signal.reason)
|
|
16
|
+
} else {
|
|
17
|
+
this.handler.onConnect(abort)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
onBodySent(chunk) {
|
|
22
|
+
return this.handler.onBodySent(chunk)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
26
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onData(chunk) {
|
|
30
|
+
return this.handler.onData(chunk)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onComplete(rawTrailers) {
|
|
34
|
+
this.signal.removeEventListener('abort', this.abort)
|
|
35
|
+
return this.handler.onComplete(rawTrailers)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onError(err) {
|
|
39
|
+
this.signal.removeEventListener('abort', this.abort)
|
|
40
|
+
return this.handler.onError(err)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports =
|
|
45
|
+
(dispatch) =>
|
|
46
|
+
({ signal, ...opts }, handler) =>
|
|
47
|
+
signal ? dispatch(opts, new Handler(opts, signal, { handler })) : dispatch(opts, handler)
|
package/undici/utils.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
function isDisturbed(body) {
|
|
2
|
+
return !(body == null || typeof body === 'string' || Buffer.isBuffer(body))
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function parseContentRange(range) {
|
|
6
|
+
if (typeof range !== 'string') {
|
|
7
|
+
return null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const m = range.match(/^bytes (\d+)-(\d+)?\/(\d+|\*)$/)
|
|
11
|
+
if (!m) {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const start = m[1] == null ? null : Number(m[1])
|
|
16
|
+
if (!Number.isFinite(start)) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const end = m[2] == null ? null : Number(m[2])
|
|
21
|
+
if (end !== null && !Number.isFinite(end)) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const size = m[2] === '*' ? null : Number(m[2])
|
|
26
|
+
if (size !== null && !Number.isFinite(size)) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { start, end: end + 1, size }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findHeader(rawHeaders, name) {
|
|
34
|
+
const len = name.length
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < rawHeaders.length; i += 2) {
|
|
37
|
+
const key = rawHeaders[i + 0]
|
|
38
|
+
if (key.length === len && key.toString().toLowerCase() === name) {
|
|
39
|
+
return rawHeaders[i + 1].toString()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function retryAfter(err, retryCount, opts) {
|
|
46
|
+
if (opts.retry === null || opts.retry === false) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof opts.retry === 'function') {
|
|
51
|
+
const ret = opts.retry(err, retryCount)
|
|
52
|
+
if (ret != null) {
|
|
53
|
+
return ret
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const retryMax = opts.retry?.count ?? opts.maxRetries ?? 8
|
|
58
|
+
|
|
59
|
+
if (retryCount > retryMax) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (err.statusCode && [420, 429, 502, 503, 504].includes(err.statusCode)) {
|
|
64
|
+
const retryAfter = err.headers['retry-after'] ? err.headers['retry-after'] * 1e3 : null
|
|
65
|
+
return retryAfter ?? Math.min(10e3, retryCount * 1e3)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
err.code &&
|
|
70
|
+
[
|
|
71
|
+
'ECONNRESET',
|
|
72
|
+
'ECONNREFUSED',
|
|
73
|
+
'ENOTFOUND',
|
|
74
|
+
'ENETDOWN',
|
|
75
|
+
'ENETUNREACH',
|
|
76
|
+
'EHOSTDOWN',
|
|
77
|
+
'EHOSTUNREACH',
|
|
78
|
+
'EPIPE',
|
|
79
|
+
].includes(err.code)
|
|
80
|
+
) {
|
|
81
|
+
return Math.min(10e3, retryCount * 1e3)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (err.message && ['other side closed'].includes(err.message)) {
|
|
85
|
+
return Math.min(10e3, retryCount * 1e3)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseURL(url) {
|
|
92
|
+
if (typeof url === 'string') {
|
|
93
|
+
url = new URL(url)
|
|
94
|
+
|
|
95
|
+
if (!/^https?:/.test(url.origin || url.protocol)) {
|
|
96
|
+
throw new Error('Invalid URL protocol: the URL must start with `http:` or `https:`.')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return url
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!url || typeof url !== 'object') {
|
|
103
|
+
throw new Error('Invalid URL: The URL argument must be a non-null object.')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
'Invalid URL: port must be a valid integer or a string representation of an integer.',
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (url.path != null && typeof url.path !== 'string') {
|
|
113
|
+
throw new Error('Invalid URL path: the path must be a string or null/undefined.')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (url.pathname != null && typeof url.pathname !== 'string') {
|
|
117
|
+
throw new Error('Invalid URL pathname: the pathname must be a string or null/undefined.')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (url.hostname != null && typeof url.hostname !== 'string') {
|
|
121
|
+
throw new Error('Invalid URL hostname: the hostname must be a string or null/undefined.')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (url.origin != null && typeof url.origin !== 'string') {
|
|
125
|
+
throw new Error('Invalid URL origin: the origin must be a string or null/undefined.')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!/^https?:/.test(url.origin || url.protocol)) {
|
|
129
|
+
throw new Error('Invalid URL protocol: the URL must start with `http:` or `https:`.')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!(url instanceof URL)) {
|
|
133
|
+
const port = url.port != null ? url.port : url.protocol === 'https:' ? 443 : 80
|
|
134
|
+
let origin = url.origin != null ? url.origin : `${url.protocol}//${url.hostname}:${port}`
|
|
135
|
+
let path = url.path != null ? url.path : `${url.pathname || ''}${url.search || ''}`
|
|
136
|
+
|
|
137
|
+
if (origin.endsWith('/')) {
|
|
138
|
+
origin = origin.substring(0, origin.length - 1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (path && !path.startsWith('/')) {
|
|
142
|
+
path = `/${path}`
|
|
143
|
+
}
|
|
144
|
+
// new URL(path, origin) is unsafe when `path` contains an absolute URL
|
|
145
|
+
// From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
|
|
146
|
+
// If first parameter is a relative URL, second param is required, and will be used as the base URL.
|
|
147
|
+
// If first parameter is an absolute URL, a given second param will be ignored.
|
|
148
|
+
url = new URL(origin + path)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return url
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseOrigin(url) {
|
|
155
|
+
url = module.exports.parseURL(url)
|
|
156
|
+
|
|
157
|
+
if (url.pathname !== '/' || url.search || url.hash) {
|
|
158
|
+
throw new Error('invalid url')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return url
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
isDisturbed,
|
|
166
|
+
parseContentRange,
|
|
167
|
+
findHeader,
|
|
168
|
+
retryAfter,
|
|
169
|
+
parseURL,
|
|
170
|
+
parseOrigin,
|
|
171
|
+
}
|
package/undici.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
const assert = require('assert')
|
|
2
|
-
const { errorMonitor } = require('node:events')
|
|
3
|
-
const tp = require('timers/promises')
|
|
4
|
-
const xuid = require('xuid')
|
|
5
|
-
const { isReadableNodeStream, readableStreamLength } = require('./stream')
|
|
6
|
-
const undici = require('undici')
|
|
7
|
-
const stream = require('stream')
|
|
8
|
-
const omitEmpty = require('omit-empty')
|
|
9
|
-
|
|
10
|
-
module.exports.request = async function request(
|
|
11
|
-
url,
|
|
12
|
-
{
|
|
13
|
-
logger,
|
|
14
|
-
id = xuid(),
|
|
15
|
-
retry,
|
|
16
|
-
maxRedirections: _maxRedirections,
|
|
17
|
-
idempotent,
|
|
18
|
-
redirect,
|
|
19
|
-
dispatcher,
|
|
20
|
-
signal,
|
|
21
|
-
headersTimeout,
|
|
22
|
-
bodyTimeout,
|
|
23
|
-
reset = false,
|
|
24
|
-
body,
|
|
25
|
-
method = body ? 'POST' : 'GET',
|
|
26
|
-
userAgent,
|
|
27
|
-
headers,
|
|
28
|
-
dump = method === 'HEAD',
|
|
29
|
-
}
|
|
30
|
-
) {
|
|
31
|
-
if (retry === false) {
|
|
32
|
-
retry = { count: 0 }
|
|
33
|
-
} else if (typeof retry === 'number') {
|
|
34
|
-
retry = { count: retry }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (redirect === false) {
|
|
38
|
-
redirect = { count: 0 }
|
|
39
|
-
} else if (typeof redirect === 'number') {
|
|
40
|
-
redirect = { count: redirect }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const { count: maxRedirections = _maxRedirections ?? 3 } = redirect ?? {}
|
|
44
|
-
const {
|
|
45
|
-
count: maxRetries = 8,
|
|
46
|
-
method: retryMethod = ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE', 'PATCH'],
|
|
47
|
-
status: retryStatus = [420, 429, 502, 503, 504],
|
|
48
|
-
code: retryCode = [
|
|
49
|
-
'ECONNRESET',
|
|
50
|
-
'ECONNREFUSED',
|
|
51
|
-
'ENOTFOUND',
|
|
52
|
-
'ENETDOWN',
|
|
53
|
-
'ENETUNREACH',
|
|
54
|
-
'EHOSTDOWN',
|
|
55
|
-
'EHOSTUNREACH',
|
|
56
|
-
'EPIPE',
|
|
57
|
-
],
|
|
58
|
-
message: retryMessage = ['other side closed'],
|
|
59
|
-
} = retry ?? {}
|
|
60
|
-
|
|
61
|
-
if (readableStreamLength(body) === 0) {
|
|
62
|
-
body.on('error', () => {})
|
|
63
|
-
body = null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const ureq = {
|
|
67
|
-
url,
|
|
68
|
-
method,
|
|
69
|
-
body,
|
|
70
|
-
headers: omitEmpty({
|
|
71
|
-
'request-id': id,
|
|
72
|
-
'user-agent': userAgent,
|
|
73
|
-
...headers,
|
|
74
|
-
}),
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let upstreamLogger = logger?.child({ ureq })
|
|
78
|
-
|
|
79
|
-
upstreamLogger?.debug('upstream request started')
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
/* eslint-disable no-unreachable-loop */
|
|
83
|
-
for (let retryCount = 0; true; retryCount++) {
|
|
84
|
-
let ures
|
|
85
|
-
try {
|
|
86
|
-
ures = await undici.request(url, {
|
|
87
|
-
method,
|
|
88
|
-
reset,
|
|
89
|
-
body,
|
|
90
|
-
headers,
|
|
91
|
-
signal,
|
|
92
|
-
dispatcher,
|
|
93
|
-
maxRedirections,
|
|
94
|
-
throwOnError: true,
|
|
95
|
-
headersTimeout,
|
|
96
|
-
bodyTimeout,
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
upstreamLogger = upstreamLogger?.child({ ures })
|
|
100
|
-
|
|
101
|
-
upstreamLogger?.debug('upstream request response')
|
|
102
|
-
|
|
103
|
-
if (ures.statusCode >= 300 && ures.statusCode < 400) {
|
|
104
|
-
throw new Error('maxRedirections exceeded')
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
assert(ures.statusCode >= 200 && ures.statusCode < 300)
|
|
108
|
-
|
|
109
|
-
if (dump) {
|
|
110
|
-
await ures.body.dump()
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
ures.body.on(errorMonitor, (err) => {
|
|
114
|
-
upstreamLogger?.debug({ err }, 'upstream request response body failed')
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
// TODO (fix): Wrap response to handle error that can continue with range request...
|
|
118
|
-
|
|
119
|
-
return ures
|
|
120
|
-
} catch (err) {
|
|
121
|
-
await ures?.body.dump()
|
|
122
|
-
|
|
123
|
-
if (retryCount >= maxRetries) {
|
|
124
|
-
throw err
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
body != null &&
|
|
129
|
-
typeof body !== 'string' &&
|
|
130
|
-
!Buffer.isBuffer(body) &&
|
|
131
|
-
(!isReadableNodeStream(body) || stream.isDisturbed(body))
|
|
132
|
-
) {
|
|
133
|
-
throw err
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (!retryMethod.includes(method) && !idempotent) {
|
|
137
|
-
throw err
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
!retryCode.includes(err.code) &&
|
|
142
|
-
!retryMessage.includes(err.message) &&
|
|
143
|
-
!retryStatus.includes(err.statusCode)
|
|
144
|
-
) {
|
|
145
|
-
throw err
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const delay =
|
|
149
|
-
parseInt(err.headers?.['Retry-After']) * 1e3 || Math.min(10e3, retryCount * 1e3 + 1e3)
|
|
150
|
-
|
|
151
|
-
upstreamLogger?.warn({ err, retryCount, delay }, 'upstream request retrying')
|
|
152
|
-
|
|
153
|
-
await tp.setTimeout(delay, undefined, { signal })
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
} catch (err) {
|
|
157
|
-
upstreamLogger?.error({ err }, 'upstream request failed')
|
|
158
|
-
throw err
|
|
159
|
-
}
|
|
160
|
-
}
|