@nxtedition/nxt-undici 2.0.46 → 2.0.47
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 +12 -14
- package/lib/interceptor/cache.js +30 -29
- package/lib/interceptor/log.js +61 -44
- package/lib/interceptor/proxy.js +16 -28
- package/lib/interceptor/redirect.js +4 -1
- package/lib/interceptor/response-content.js +48 -31
- package/lib/interceptor/response-error.js +56 -73
- package/lib/interceptor/response-retry.js +179 -80
- package/lib/utils.js +30 -5
- package/package.json +1 -1
- package/lib/interceptor/response-retry-body.js +0 -193
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import assert from 'node:assert'
|
|
|
2
2
|
import createError from 'http-errors'
|
|
3
3
|
import undici from 'undici'
|
|
4
4
|
import { findHeader, parseHeaders, AbortError, isStream } from './utils.js'
|
|
5
|
-
import { BodyReadable
|
|
5
|
+
import { BodyReadable } from './readable.js'
|
|
6
6
|
|
|
7
7
|
const dispatcherCache = new WeakMap()
|
|
8
8
|
|
|
@@ -19,14 +19,13 @@ function genReqId() {
|
|
|
19
19
|
return `req-${nextReqId.toString(36)}`
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
export const interceptors = {
|
|
23
23
|
responseError: (await import('./interceptor/response-error.js')).default,
|
|
24
24
|
requestBodyFactory: (await import('./interceptor/request-body-factory.js')).default,
|
|
25
25
|
responseContent: (await import('./interceptor/response-content.js')).default,
|
|
26
26
|
log: (await import('./interceptor/log.js')).default,
|
|
27
27
|
redirect: (await import('./interceptor/redirect.js')).default,
|
|
28
28
|
responseRetry: (await import('./interceptor/response-retry.js')).default,
|
|
29
|
-
responseRetryBody: (await import('./interceptor/response-retry-body.js')).default,
|
|
30
29
|
proxy: (await import('./interceptor/proxy.js')).default,
|
|
31
30
|
cache: (await import('./interceptor/cache.js')).default,
|
|
32
31
|
requestId: (await import('./interceptor/request-id.js')).default,
|
|
@@ -110,16 +109,15 @@ export async function request(url, opts) {
|
|
|
110
109
|
let dispatch = dispatcherCache.get(dispatcher)
|
|
111
110
|
if (dispatch == null) {
|
|
112
111
|
dispatch = (opts, handler) => dispatcher.dispatch(opts, handler)
|
|
113
|
-
dispatch =
|
|
114
|
-
dispatch =
|
|
115
|
-
dispatch =
|
|
116
|
-
dispatch =
|
|
117
|
-
dispatch =
|
|
118
|
-
dispatch =
|
|
119
|
-
dispatch =
|
|
120
|
-
dispatch =
|
|
121
|
-
dispatch =
|
|
122
|
-
dispatch = dispatchers.proxy(dispatch)
|
|
112
|
+
dispatch = interceptors.responseError(dispatch)
|
|
113
|
+
dispatch = interceptors.requestBodyFactory(dispatch)
|
|
114
|
+
dispatch = interceptors.log(dispatch)
|
|
115
|
+
dispatch = interceptors.requestId(dispatch)
|
|
116
|
+
dispatch = interceptors.responseRetry(dispatch)
|
|
117
|
+
dispatch = interceptors.responseContent(dispatch)
|
|
118
|
+
dispatch = interceptors.cache(dispatch)
|
|
119
|
+
dispatch = interceptors.redirect(dispatch)
|
|
120
|
+
dispatch = interceptors.proxy(dispatch)
|
|
123
121
|
dispatcherCache.set(dispatcher, dispatch)
|
|
124
122
|
}
|
|
125
123
|
|
|
@@ -192,7 +190,7 @@ export async function request(url, opts) {
|
|
|
192
190
|
const contentLength = findHeader(headers, 'content-length')
|
|
193
191
|
const contentType = findHeader(headers, 'content-type')
|
|
194
192
|
|
|
195
|
-
this.body = new
|
|
193
|
+
this.body = new BodyReadable(this, {
|
|
196
194
|
resume,
|
|
197
195
|
abort: this.abort,
|
|
198
196
|
highWaterMark: this.highWaterMark,
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -1,30 +1,35 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
import { LRUCache } from 'lru-cache'
|
|
3
|
-
import {
|
|
3
|
+
import { parseHeaders, parseCacheControl } from '../utils.js'
|
|
4
|
+
import { DecoratorHandler } from 'undici'
|
|
5
|
+
|
|
6
|
+
class CacheHandler extends DecoratorHandler {
|
|
7
|
+
#handler
|
|
8
|
+
#store
|
|
9
|
+
#key
|
|
10
|
+
#value
|
|
4
11
|
|
|
5
|
-
class CacheHandler {
|
|
6
12
|
constructor({ key, handler, store }) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
this
|
|
10
|
-
this
|
|
13
|
+
super(handler)
|
|
14
|
+
|
|
15
|
+
this.#key = key
|
|
16
|
+
this.#handler = handler
|
|
17
|
+
this.#store = store
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
onConnect(abort) {
|
|
14
|
-
|
|
15
|
-
}
|
|
21
|
+
this.#value = null
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
|
|
23
|
+
return this.#handler.onConnect(abort)
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
|
|
26
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
|
|
22
27
|
// NOTE: Only cache 307 respones for now...
|
|
23
28
|
if (statusCode !== 307) {
|
|
24
|
-
return this
|
|
29
|
+
return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
const cacheControl = parseCacheControl(
|
|
32
|
+
const cacheControl = parseCacheControl(headers['cache-control'])
|
|
28
33
|
|
|
29
34
|
if (
|
|
30
35
|
cacheControl &&
|
|
@@ -44,7 +49,7 @@ class CacheHandler {
|
|
|
44
49
|
: Number(maxAge)
|
|
45
50
|
|
|
46
51
|
if (ttl > 0) {
|
|
47
|
-
this
|
|
52
|
+
this.#value = {
|
|
48
53
|
statusCode,
|
|
49
54
|
statusMessage,
|
|
50
55
|
rawHeaders,
|
|
@@ -56,31 +61,27 @@ class CacheHandler {
|
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
return this
|
|
64
|
+
return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
onData(chunk) {
|
|
63
|
-
if (this
|
|
64
|
-
this
|
|
65
|
-
if (this
|
|
66
|
-
this
|
|
68
|
+
if (this.#value) {
|
|
69
|
+
this.#value.size += chunk.bodyLength
|
|
70
|
+
if (this.#value.size > this.#store.maxEntrySize) {
|
|
71
|
+
this.#value = null
|
|
67
72
|
} else {
|
|
68
|
-
this
|
|
73
|
+
this.#value.body.push(chunk)
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
|
-
return this
|
|
76
|
+
return this.#handler.onData(chunk)
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
onComplete(rawTrailers) {
|
|
75
|
-
if (this
|
|
76
|
-
this
|
|
77
|
-
this
|
|
80
|
+
if (this.#value) {
|
|
81
|
+
this.#value.rawTrailers = rawTrailers
|
|
82
|
+
this.#store.set(this.#key, this.#value, this.#value.ttl)
|
|
78
83
|
}
|
|
79
|
-
return this
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
onError(err) {
|
|
83
|
-
return this.handler.onError(err)
|
|
84
|
+
return this.#handler.onComplete(rawTrailers)
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
package/lib/interceptor/log.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks'
|
|
2
2
|
import { parseHeaders } from '../utils.js'
|
|
3
|
+
import { DecoratorHandler } from 'undici'
|
|
4
|
+
|
|
5
|
+
class Handler extends DecoratorHandler {
|
|
6
|
+
#handler
|
|
7
|
+
#opts
|
|
8
|
+
#abort
|
|
9
|
+
#aborted = false
|
|
10
|
+
#logger
|
|
11
|
+
#pos
|
|
12
|
+
#stats
|
|
3
13
|
|
|
4
|
-
class Handler {
|
|
5
14
|
constructor(opts, { handler }) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
this
|
|
9
|
-
this
|
|
10
|
-
this
|
|
11
|
-
this
|
|
12
|
-
|
|
15
|
+
super(handler)
|
|
16
|
+
|
|
17
|
+
this.#handler = handler
|
|
18
|
+
this.#opts = opts
|
|
19
|
+
this.#logger = opts.logger.child({ ureq: { id: opts.id } })
|
|
20
|
+
this.#stats = {
|
|
21
|
+
created: performance.now(),
|
|
13
22
|
start: -1,
|
|
14
23
|
end: -1,
|
|
15
24
|
headers: -1,
|
|
@@ -19,89 +28,97 @@ class Handler {
|
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
onConnect(abort) {
|
|
22
|
-
this
|
|
23
|
-
this
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
this.#pos = 0
|
|
32
|
+
this.#abort = abort
|
|
33
|
+
this.#stats.start = performance.now()
|
|
34
|
+
this.#stats.end = -1
|
|
35
|
+
this.#stats.headers = -1
|
|
36
|
+
this.#stats.firstBodyReceived = -1
|
|
37
|
+
this.#stats.lastBodyReceived = -1
|
|
38
|
+
|
|
39
|
+
this.#logger.debug({ ureq: this.#opts }, 'upstream request started')
|
|
40
|
+
|
|
41
|
+
return this.#handler.onConnect((reason) => {
|
|
42
|
+
this.#aborted = true
|
|
43
|
+
this.#abort(reason)
|
|
29
44
|
})
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
onUpgrade(statusCode, rawHeaders, socket, headers) {
|
|
33
|
-
this
|
|
48
|
+
this.#logger.debug('upstream request upgraded')
|
|
34
49
|
socket.on('close', () => {
|
|
35
|
-
this
|
|
50
|
+
this.#logger.debug('upstream request socket closed')
|
|
36
51
|
})
|
|
37
52
|
|
|
38
|
-
return this
|
|
53
|
+
return this.#handler.onUpgrade(statusCode, rawHeaders, socket, headers)
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
|
|
42
|
-
this
|
|
57
|
+
this.#stats.headers = performance.now() - this.#stats.start
|
|
43
58
|
|
|
44
|
-
this
|
|
59
|
+
this.#logger.debug(
|
|
45
60
|
{
|
|
46
61
|
ures: { statusCode, headers },
|
|
47
|
-
elapsedTime: this
|
|
62
|
+
elapsedTime: this.#stats.headers,
|
|
48
63
|
},
|
|
49
64
|
'upstream request response',
|
|
50
65
|
)
|
|
51
66
|
|
|
52
|
-
return this
|
|
67
|
+
return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
onData(chunk) {
|
|
56
|
-
if (this
|
|
57
|
-
this
|
|
71
|
+
if (this.#stats.firstBodyReceived === -1) {
|
|
72
|
+
this.#stats.firstBodyReceived = performance.now() - this.#stats.start
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
this
|
|
75
|
+
this.#pos += chunk.length
|
|
61
76
|
|
|
62
|
-
return this
|
|
77
|
+
return this.#handler.onData(chunk)
|
|
63
78
|
}
|
|
64
79
|
|
|
65
80
|
onComplete(rawTrailers) {
|
|
66
|
-
this
|
|
67
|
-
this
|
|
81
|
+
this.#stats.lastBodyReceived = performance.now() - this.#stats.start
|
|
82
|
+
this.#stats.end = this.#stats.lastBodyReceived
|
|
68
83
|
|
|
69
|
-
this
|
|
70
|
-
{ bytesRead: this
|
|
84
|
+
this.#logger.debug(
|
|
85
|
+
{ bytesRead: this.#pos, elapsedTime: this.#stats.end, stats: this.#stats },
|
|
71
86
|
'upstream request completed',
|
|
72
87
|
)
|
|
73
88
|
|
|
74
|
-
return this
|
|
89
|
+
return this.#handler.onComplete(rawTrailers)
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
onError(err) {
|
|
78
|
-
this
|
|
93
|
+
if (this.#stats) {
|
|
94
|
+
this.#stats.end = performance.now() - this.#stats.start
|
|
95
|
+
}
|
|
79
96
|
|
|
80
|
-
if (this
|
|
81
|
-
this
|
|
97
|
+
if (this.#aborted) {
|
|
98
|
+
this.#logger.debug(
|
|
82
99
|
{
|
|
83
|
-
ureq: this
|
|
84
|
-
bytesRead: this
|
|
85
|
-
elapsedTime: this
|
|
86
|
-
stats: this
|
|
100
|
+
ureq: this.#opts,
|
|
101
|
+
bytesRead: this.#pos,
|
|
102
|
+
elapsedTime: this.#stats.end,
|
|
103
|
+
stats: this.#stats,
|
|
87
104
|
err,
|
|
88
105
|
},
|
|
89
106
|
'upstream request aborted',
|
|
90
107
|
)
|
|
91
108
|
} else {
|
|
92
|
-
this
|
|
109
|
+
this.#logger.error(
|
|
93
110
|
{
|
|
94
|
-
ureq: this
|
|
95
|
-
bytesRead: this
|
|
96
|
-
elapsedTime: this
|
|
97
|
-
stats: this
|
|
111
|
+
ureq: this.#opts,
|
|
112
|
+
bytesRead: this.#pos,
|
|
113
|
+
elapsedTime: this.#stats.end,
|
|
114
|
+
stats: this.#stats,
|
|
98
115
|
err,
|
|
99
116
|
},
|
|
100
117
|
'upstream request failed',
|
|
101
118
|
)
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
return this
|
|
121
|
+
return this.#handler.onError(err)
|
|
105
122
|
}
|
|
106
123
|
}
|
|
107
124
|
|
package/lib/interceptor/proxy.js
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import net from 'node:net'
|
|
2
2
|
import createError from 'http-errors'
|
|
3
|
+
import { DecoratorHandler } from 'undici'
|
|
3
4
|
|
|
4
|
-
class Handler {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
5
|
+
class Handler extends DecoratorHandler {
|
|
6
|
+
#handler
|
|
7
|
+
#opts
|
|
8
|
+
|
|
9
|
+
constructor(proxyOpts, { handler }) {
|
|
10
|
+
super(handler)
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
this.#handler = handler
|
|
13
|
+
this.#opts = proxyOpts
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
onUpgrade(statusCode, rawHeaders, socket) {
|
|
15
|
-
return this
|
|
17
|
+
return this.#handler.onUpgrade(
|
|
16
18
|
statusCode,
|
|
17
19
|
reduceHeaders(
|
|
18
20
|
{
|
|
19
21
|
headers: rawHeaders,
|
|
20
|
-
httpVersion: this
|
|
22
|
+
httpVersion: this.#opts.httpVersion ?? this.#opts.req?.httpVersion,
|
|
21
23
|
socket: null,
|
|
22
|
-
proxyName: this
|
|
24
|
+
proxyName: this.#opts.name,
|
|
23
25
|
},
|
|
24
26
|
(acc, key, val) => {
|
|
25
27
|
acc.push(key, val)
|
|
@@ -32,14 +34,14 @@ class Handler {
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
35
|
-
return this
|
|
37
|
+
return this.#handler.onHeaders(
|
|
36
38
|
statusCode,
|
|
37
39
|
reduceHeaders(
|
|
38
40
|
{
|
|
39
41
|
headers: rawHeaders,
|
|
40
|
-
httpVersion: this
|
|
42
|
+
httpVersion: this.#opts.httpVersion ?? this.#opts.req?.httpVersion,
|
|
41
43
|
socket: null,
|
|
42
|
-
proxyName: this
|
|
44
|
+
proxyName: this.#opts.name,
|
|
43
45
|
},
|
|
44
46
|
(acc, key, val) => {
|
|
45
47
|
acc.push(key, val)
|
|
@@ -51,18 +53,6 @@ class Handler {
|
|
|
51
53
|
statusMessage,
|
|
52
54
|
)
|
|
53
55
|
}
|
|
54
|
-
|
|
55
|
-
onData(chunk) {
|
|
56
|
-
return this.handler.onData(chunk)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
onComplete(rawTrailers) {
|
|
60
|
-
return this.handler.onComplete(rawTrailers)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
onError(err) {
|
|
64
|
-
return this.handler.onError(err)
|
|
65
|
-
}
|
|
66
56
|
}
|
|
67
57
|
|
|
68
58
|
export default (dispatch) => (opts, handler) => {
|
|
@@ -84,9 +74,7 @@ export default (dispatch) => (opts, handler) => {
|
|
|
84
74
|
{},
|
|
85
75
|
)
|
|
86
76
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return dispatch(opts, new Handler(opts, { handler }))
|
|
77
|
+
return dispatch({ ...opts, headers }, new Handler(opts.proxy, { handler }))
|
|
90
78
|
}
|
|
91
79
|
|
|
92
80
|
// This expression matches hop-by-hop headers.
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
import { findHeader, isDisturbed, parseURL } from '../utils.js'
|
|
3
|
+
import { DecoratorHandler } from 'undici'
|
|
3
4
|
|
|
4
5
|
const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
|
|
5
6
|
|
|
6
|
-
class Handler {
|
|
7
|
+
class Handler extends DecoratorHandler {
|
|
7
8
|
constructor(opts, { dispatch, handler }) {
|
|
9
|
+
super(handler)
|
|
10
|
+
|
|
8
11
|
this.dispatch = dispatch
|
|
9
12
|
this.handler = handler
|
|
10
13
|
this.opts = opts
|
|
@@ -1,63 +1,80 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
|
-
import
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { parseHeaders } from '../utils.js'
|
|
4
|
+
import { DecoratorHandler } from 'undici'
|
|
5
|
+
|
|
6
|
+
class Handler extends DecoratorHandler {
|
|
7
|
+
#handler
|
|
8
|
+
|
|
9
|
+
#contentMD5
|
|
10
|
+
#contentLength
|
|
11
|
+
#hasher
|
|
12
|
+
#pos
|
|
13
|
+
#errored
|
|
3
14
|
|
|
4
|
-
class Handler {
|
|
5
15
|
constructor(opts, { handler }) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
this
|
|
9
|
-
this.length = null
|
|
10
|
-
this.hasher = null
|
|
11
|
-
this.pos = 0
|
|
16
|
+
super(handler)
|
|
17
|
+
|
|
18
|
+
this.#handler = handler
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
onConnect(abort) {
|
|
15
|
-
|
|
16
|
-
}
|
|
22
|
+
assert(!this.#pos)
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
this.#contentMD5 = null
|
|
25
|
+
this.#contentLength = null
|
|
26
|
+
this.#hasher = null
|
|
27
|
+
this.#pos = 0
|
|
28
|
+
this.#errored = false
|
|
29
|
+
|
|
30
|
+
this.#handler.onConnect(abort)
|
|
20
31
|
}
|
|
21
32
|
|
|
22
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
|
|
23
|
-
this
|
|
24
|
-
this
|
|
25
|
-
this
|
|
33
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
|
|
34
|
+
this.#contentMD5 = headers ? headers['content-md5'] : headers['content-md5']
|
|
35
|
+
this.#contentLength = headers ? headers['content-length'] : headers['content-length']
|
|
36
|
+
this.#hasher = this.#contentMD5 != null ? crypto.createHash('md5') : null
|
|
26
37
|
|
|
27
|
-
return this
|
|
38
|
+
return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
onData(chunk) {
|
|
31
|
-
this
|
|
32
|
-
this
|
|
42
|
+
this.#pos += chunk.length
|
|
43
|
+
this.#hasher?.update(chunk)
|
|
33
44
|
|
|
34
|
-
return this
|
|
45
|
+
return this.#handler.onData(chunk)
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
onComplete(rawTrailers) {
|
|
38
|
-
const
|
|
49
|
+
const contentMD5 = this.#hasher?.digest('base64')
|
|
39
50
|
|
|
40
|
-
if (this
|
|
41
|
-
|
|
51
|
+
if (this.#contentLength != null && this.#pos !== Number(this.#contentLength)) {
|
|
52
|
+
this.#errored = true
|
|
53
|
+
this.#handler.onError(
|
|
42
54
|
Object.assign(new Error('Request Content-Length mismatch'), {
|
|
43
|
-
expected: Number(this
|
|
44
|
-
actual: this
|
|
55
|
+
expected: Number(this.#contentLength),
|
|
56
|
+
actual: this.#pos,
|
|
45
57
|
}),
|
|
46
58
|
)
|
|
47
|
-
} else if (this
|
|
48
|
-
|
|
59
|
+
} else if (this.#contentMD5 != null && contentMD5 !== this.#contentMD5) {
|
|
60
|
+
this.#errored = true
|
|
61
|
+
this.#handler.onError(
|
|
49
62
|
Object.assign(new Error('Request Content-MD5 mismatch'), {
|
|
50
|
-
expected: this
|
|
51
|
-
actual:
|
|
63
|
+
expected: this.#contentMD5,
|
|
64
|
+
actual: contentMD5,
|
|
52
65
|
}),
|
|
53
66
|
)
|
|
54
67
|
} else {
|
|
55
|
-
return this
|
|
68
|
+
return this.#handler.onComplete(rawTrailers)
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
onError(err) {
|
|
60
|
-
this
|
|
73
|
+
if (this.#errored) {
|
|
74
|
+
// Do nothing...
|
|
75
|
+
} else {
|
|
76
|
+
this.#handler.onError(err)
|
|
77
|
+
}
|
|
61
78
|
}
|
|
62
79
|
}
|
|
63
80
|
|
|
@@ -1,106 +1,89 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseHeaders } from '../utils.js'
|
|
2
2
|
import createHttpError from 'http-errors'
|
|
3
|
+
import { DecoratorHandler } from 'undici'
|
|
3
4
|
|
|
4
|
-
class Handler {
|
|
5
|
-
handler
|
|
6
|
-
opts
|
|
5
|
+
class Handler extends DecoratorHandler {
|
|
6
|
+
#handler
|
|
7
7
|
|
|
8
|
-
statusCode
|
|
9
|
-
contentType
|
|
10
|
-
decoder
|
|
11
|
-
headers
|
|
12
|
-
body
|
|
8
|
+
#statusCode
|
|
9
|
+
#contentType
|
|
10
|
+
#decoder
|
|
11
|
+
#headers
|
|
12
|
+
#body
|
|
13
|
+
#errored
|
|
13
14
|
|
|
14
15
|
constructor(opts, { handler }) {
|
|
15
|
-
|
|
16
|
-
this.opts = opts
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
onConnect(abort) {
|
|
20
|
-
this.statusCode = 0
|
|
21
|
-
this.contentType = null
|
|
22
|
-
this.decoder = null
|
|
23
|
-
this.headers = null
|
|
24
|
-
this.body = null
|
|
16
|
+
super(handler)
|
|
25
17
|
|
|
26
|
-
|
|
18
|
+
this.#handler = handler
|
|
27
19
|
}
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
onConnect(abort) {
|
|
22
|
+
this.#statusCode = 0
|
|
23
|
+
this.#contentType = null
|
|
24
|
+
this.#decoder = null
|
|
25
|
+
this.#headers = null
|
|
26
|
+
this.#body = ''
|
|
27
|
+
this.#errored = false
|
|
28
|
+
|
|
29
|
+
return this.#handler.onConnect(abort)
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
this.#statusCode = statusCode
|
|
34
|
+
this.#headers = headers
|
|
35
|
+
this.#contentType = headers['content-type']
|
|
36
|
+
|
|
37
|
+
if (this.#statusCode >= 400) {
|
|
38
|
+
// TODO (fix): Check content length
|
|
39
|
+
if (this.#contentType === 'application/json' || this.#contentType === 'text/plain') {
|
|
40
|
+
this.#decoder = new TextDecoder('utf-8')
|
|
41
41
|
}
|
|
42
42
|
} else {
|
|
43
|
-
return this
|
|
43
|
+
return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
onData(chunk) {
|
|
48
|
-
if (this
|
|
49
|
-
|
|
50
|
-
// TODO (fix): Limit body size?
|
|
51
|
-
this.body += this.decoder.decode(chunk, { stream: true })
|
|
52
|
-
}
|
|
53
|
-
return true
|
|
48
|
+
if (this.#statusCode >= 400) {
|
|
49
|
+
this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? ''
|
|
54
50
|
} else {
|
|
55
|
-
return this
|
|
51
|
+
return this.#handler.onData(chunk)
|
|
56
52
|
}
|
|
57
53
|
}
|
|
58
54
|
|
|
59
55
|
onComplete(rawTrailers) {
|
|
60
|
-
this
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (this.statusCode >= 400) {
|
|
69
|
-
if (this.decoder != null) {
|
|
70
|
-
this.body += this.decoder.decode(undefined, { stream: false })
|
|
71
|
-
if (this.contentType === 'application/json') {
|
|
72
|
-
try {
|
|
73
|
-
this.body = JSON.parse(this.body)
|
|
74
|
-
} catch {
|
|
75
|
-
// Do nothing...
|
|
76
|
-
}
|
|
56
|
+
if (this.#statusCode >= 400) {
|
|
57
|
+
this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
|
|
58
|
+
|
|
59
|
+
if (this.#contentType === 'application/json') {
|
|
60
|
+
try {
|
|
61
|
+
this.#body = JSON.parse(this.#body)
|
|
62
|
+
} catch {
|
|
63
|
+
// Do nothing...
|
|
77
64
|
}
|
|
78
65
|
}
|
|
79
66
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (err) {
|
|
88
|
-
this.handler.onError(
|
|
89
|
-
Object.assign(err, {
|
|
90
|
-
ureq: {
|
|
91
|
-
origin: this.opts.origin,
|
|
92
|
-
path: this.opts.path,
|
|
93
|
-
method: this.opts.method,
|
|
94
|
-
headers: this.opts.headers,
|
|
95
|
-
},
|
|
96
|
-
ures: { statusCode: this.statusCode, headers: this.headers, body: this.body },
|
|
67
|
+
this.#errored = true
|
|
68
|
+
this.#handler.onError(
|
|
69
|
+
Object.assign(createHttpError(this.#statusCode), {
|
|
70
|
+
reason: this.#body?.reason,
|
|
71
|
+
error: this.#body?.error,
|
|
72
|
+
headers: this.#headers,
|
|
73
|
+
body: this.#body,
|
|
97
74
|
}),
|
|
98
75
|
)
|
|
99
76
|
} else {
|
|
100
|
-
this
|
|
77
|
+
this.#handler.onComplete(rawTrailers)
|
|
101
78
|
}
|
|
79
|
+
}
|
|
102
80
|
|
|
103
|
-
|
|
81
|
+
onError(err) {
|
|
82
|
+
if (this.#errored) {
|
|
83
|
+
// Do nothing...
|
|
84
|
+
} else {
|
|
85
|
+
this.#handler.onError(err)
|
|
86
|
+
}
|
|
104
87
|
}
|
|
105
88
|
}
|
|
106
89
|
|
|
@@ -1,121 +1,220 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
-
import { isDisturbed, retry as retryFn } from '../utils.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
2
|
+
import { isDisturbed, parseHeaders, parseRangeHeader, retry as retryFn } from '../utils.js'
|
|
3
|
+
import { DecoratorHandler } from 'undici'
|
|
4
|
+
|
|
5
|
+
// TODO (fix): What about onUpgrade?
|
|
6
|
+
class Handler extends DecoratorHandler {
|
|
7
|
+
#handler
|
|
8
|
+
#dispatch
|
|
9
|
+
#opts
|
|
10
|
+
|
|
11
|
+
#retryCount = 0
|
|
12
|
+
#headersSent = false
|
|
13
|
+
#errorSent = false
|
|
14
|
+
|
|
15
|
+
#abort
|
|
16
|
+
#aborted = false
|
|
17
|
+
#reason = null
|
|
18
|
+
|
|
19
|
+
#pos
|
|
20
|
+
#end
|
|
21
|
+
#etag
|
|
22
|
+
#error
|
|
23
|
+
|
|
24
|
+
constructor(opts, { handler, dispatch }) {
|
|
25
|
+
super(handler)
|
|
26
|
+
|
|
27
|
+
this.#handler = handler
|
|
28
|
+
this.#dispatch = dispatch
|
|
29
|
+
this.#opts = opts
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
onConnect(abort) {
|
|
33
|
-
if (this
|
|
34
|
-
|
|
33
|
+
if (!this.#headersSent) {
|
|
34
|
+
this.#pos = null
|
|
35
|
+
this.#end = null
|
|
36
|
+
this.#etag = null
|
|
37
|
+
this.#error = null
|
|
38
|
+
|
|
39
|
+
this.#handler.onConnect((reason) => {
|
|
40
|
+
if (!this.#aborted) {
|
|
41
|
+
this.#aborted = true
|
|
42
|
+
if (this.#abort) {
|
|
43
|
+
this.#abort(reason)
|
|
44
|
+
} else {
|
|
45
|
+
this.#reason = reason
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this.#aborted) {
|
|
52
|
+
abort(this.#reason)
|
|
35
53
|
} else {
|
|
36
|
-
this
|
|
54
|
+
this.#abort = abort
|
|
37
55
|
}
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
|
|
59
|
+
if (this.#error == null) {
|
|
60
|
+
assert(this.#etag == null)
|
|
61
|
+
assert(this.#pos == null)
|
|
62
|
+
assert(this.#end == null)
|
|
63
|
+
assert(this.#headersSent === false)
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|
|
65
|
+
if (headers.trailer) {
|
|
66
|
+
return this.#onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
67
|
+
}
|
|
46
68
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.headers = headers
|
|
69
|
+
const contentLength = headers['content-length'] ? Number(headers['content-length']) : null
|
|
70
|
+
if (contentLength != null && !Number.isFinite(contentLength)) {
|
|
71
|
+
return this.#onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
72
|
+
}
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
if (statusCode === 206) {
|
|
75
|
+
const range = parseRangeHeader(headers['content-range'])
|
|
76
|
+
if (!range) {
|
|
77
|
+
return this.#onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
78
|
+
}
|
|
55
79
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
const { start, size, end = size } = range
|
|
81
|
+
|
|
82
|
+
assert(start != null && Number.isFinite(start), 'content-range mismatch')
|
|
83
|
+
assert(end != null && Number.isFinite(end), 'invalid content-length')
|
|
84
|
+
assert(
|
|
85
|
+
contentLength == null || end == null || contentLength === end + 1 - start,
|
|
86
|
+
'content-range mismatch',
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
this.#pos = start
|
|
90
|
+
this.#end = end ?? contentLength
|
|
91
|
+
this.#etag = headers.etag
|
|
92
|
+
} else if (statusCode === 200) {
|
|
93
|
+
this.#pos = 0
|
|
94
|
+
this.#end = contentLength
|
|
95
|
+
this.#etag = headers.etag
|
|
96
|
+
} else {
|
|
97
|
+
return this.#onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
98
|
+
}
|
|
60
99
|
|
|
61
|
-
|
|
62
|
-
|
|
100
|
+
assert(Number.isFinite(this.#pos))
|
|
101
|
+
assert(this.#end == null || Number.isFinite(this.#end))
|
|
102
|
+
|
|
103
|
+
return this.#onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
104
|
+
} else if (statusCode === 206 || (this.#pos === 0 && statusCode === 200)) {
|
|
105
|
+
assert(this.#etag != null || !this.#pos)
|
|
106
|
+
|
|
107
|
+
const etag = headers.etag
|
|
108
|
+
if (this.#pos > 0 && this.#etag !== etag) {
|
|
109
|
+
throw this.#error
|
|
110
|
+
}
|
|
63
111
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
112
|
+
const contentRange = parseRangeHeader(headers['content-range'])
|
|
113
|
+
if (!contentRange) {
|
|
114
|
+
throw this.#error
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { start, size, end = size } = contentRange
|
|
118
|
+
assert(this.#pos === start, 'content-range mismatch')
|
|
119
|
+
assert(this.#end == null || this.#end === end, 'content-range mismatch')
|
|
120
|
+
|
|
121
|
+
// TODO (fix): What if we were paused before the error?
|
|
122
|
+
return true
|
|
123
|
+
} else {
|
|
124
|
+
throw this.#error
|
|
67
125
|
}
|
|
126
|
+
}
|
|
68
127
|
|
|
69
|
-
|
|
128
|
+
onData(chunk) {
|
|
129
|
+
if (this.#pos != null) {
|
|
130
|
+
this.#pos += chunk.byteLength
|
|
131
|
+
}
|
|
132
|
+
return this.#handler.onData(chunk)
|
|
70
133
|
}
|
|
71
134
|
|
|
72
135
|
onError(err) {
|
|
73
|
-
if (this
|
|
74
|
-
|
|
136
|
+
if (this.#aborted || isDisturbed(this.#opts.body) || (this.#pos && !this.#etag)) {
|
|
137
|
+
this.#onError(err)
|
|
138
|
+
return
|
|
75
139
|
}
|
|
76
140
|
|
|
77
|
-
const retryPromise = retryFn(err, this
|
|
141
|
+
const retryPromise = retryFn(err, this.#retryCount++, this.#opts)
|
|
78
142
|
if (retryPromise == null) {
|
|
79
|
-
|
|
143
|
+
this.#onError(err)
|
|
144
|
+
return
|
|
80
145
|
}
|
|
81
146
|
|
|
82
|
-
this
|
|
147
|
+
this.#error = err
|
|
83
148
|
|
|
84
149
|
retryPromise
|
|
85
150
|
.then(() => {
|
|
86
|
-
if (
|
|
87
|
-
this
|
|
151
|
+
if (this.#aborted) {
|
|
152
|
+
this.#onError(this.#reason)
|
|
153
|
+
} else if (isDisturbed(this.#opts.body)) {
|
|
154
|
+
this.#onError(this.#error)
|
|
155
|
+
} else if (!this.#headersSent) {
|
|
156
|
+
this.#opts.logger?.debug({ retryCount: this.#retryCount }, 'retry response headers')
|
|
157
|
+
this.#dispatch(this.#opts, this)
|
|
158
|
+
} else {
|
|
159
|
+
assert(Number.isFinite(this.#pos))
|
|
160
|
+
assert(this.#end == null || Number.isFinite(this.#end))
|
|
161
|
+
|
|
162
|
+
this.#opts = {
|
|
163
|
+
...this.#opts,
|
|
164
|
+
headers: {
|
|
165
|
+
...this.#opts.headers,
|
|
166
|
+
'if-match': this.#etag,
|
|
167
|
+
range: `bytes=${this.#pos}-${this.#end ?? ''}`,
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.#opts.logger?.debug({ retryCount: this.#retryCount }, 'retry response body')
|
|
172
|
+
this.#dispatch(this.#opts, this)
|
|
88
173
|
}
|
|
89
174
|
})
|
|
90
175
|
.catch((err) => {
|
|
91
|
-
if (!this
|
|
92
|
-
this
|
|
176
|
+
if (!this.#errorSent) {
|
|
177
|
+
this.#onError(err)
|
|
93
178
|
}
|
|
94
179
|
})
|
|
95
180
|
}
|
|
96
181
|
|
|
97
|
-
|
|
98
|
-
assert(!this
|
|
99
|
-
|
|
100
|
-
this.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
this.statusCode = null
|
|
110
|
-
this.rawHeaders = null
|
|
111
|
-
this.resume = null
|
|
112
|
-
this.statusMessage = null
|
|
113
|
-
this.headers = null
|
|
182
|
+
#onError(err) {
|
|
183
|
+
assert(!this.#errorSent)
|
|
184
|
+
this.#errorSent = true
|
|
185
|
+
this.#handler.onError(err)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#onHeaders(...args) {
|
|
189
|
+
assert(!this.#headersSent)
|
|
190
|
+
this.#headersSent = true
|
|
191
|
+
return this.#handler.onHeaders(...args)
|
|
114
192
|
}
|
|
115
193
|
}
|
|
116
194
|
|
|
117
195
|
export default (dispatch) => (opts, handler) => {
|
|
118
|
-
return opts.retry
|
|
196
|
+
return opts.idempotent && opts.retry && opts.method === 'GET' && !opts.upgrade
|
|
119
197
|
? dispatch(opts, new Handler(opts, { handler, dispatch }))
|
|
120
198
|
: dispatch(opts, handler)
|
|
121
199
|
}
|
|
200
|
+
|
|
201
|
+
export function isConnectionError(err) {
|
|
202
|
+
// AWS compat.
|
|
203
|
+
const statusCode = err?.statusCode ?? err?.$metadata?.httpStatusCode
|
|
204
|
+
return err
|
|
205
|
+
? err.code === 'ECONNRESET' ||
|
|
206
|
+
err.code === 'ECONNREFUSED' ||
|
|
207
|
+
err.code === 'ENOTFOUND' ||
|
|
208
|
+
err.code === 'ENETDOWN' ||
|
|
209
|
+
err.code === 'ENETUNREACH' ||
|
|
210
|
+
err.code === 'EHOSTDOWN' ||
|
|
211
|
+
err.code === 'EHOSTUNREACH' ||
|
|
212
|
+
err.code === 'EPIPE' ||
|
|
213
|
+
err.message === 'other side closed' ||
|
|
214
|
+
statusCode === 420 ||
|
|
215
|
+
statusCode === 429 ||
|
|
216
|
+
statusCode === 502 ||
|
|
217
|
+
statusCode === 503 ||
|
|
218
|
+
statusCode === 504
|
|
219
|
+
: false
|
|
220
|
+
}
|
package/lib/utils.js
CHANGED
|
@@ -6,6 +6,21 @@ export function parseCacheControl(str) {
|
|
|
6
6
|
return str ? cacheControlParser.parse(str) : null
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
// Parsed accordingly to RFC 9110
|
|
10
|
+
// https://www.rfc-editor.org/rfc/rfc9110#field.content-range
|
|
11
|
+
export function parseRangeHeader(range) {
|
|
12
|
+
if (range == null || range === '') return { start: 0, end: null, size: null }
|
|
13
|
+
|
|
14
|
+
const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null
|
|
15
|
+
return m
|
|
16
|
+
? {
|
|
17
|
+
start: parseInt(m[1]),
|
|
18
|
+
end: m[2] ? parseInt(m[2]) : null,
|
|
19
|
+
size: m[3] ? parseInt(m[3]) : null,
|
|
20
|
+
}
|
|
21
|
+
: null
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
export function isDisturbed(body) {
|
|
10
25
|
if (
|
|
11
26
|
body == null ||
|
|
@@ -78,12 +93,20 @@ export function findHeader(headers, name) {
|
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
export function retry(err, retryCount, opts) {
|
|
81
|
-
if (opts.retry
|
|
96
|
+
if (!opts.retry) {
|
|
82
97
|
return null
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
if (typeof opts.retry === 'function') {
|
|
86
|
-
|
|
101
|
+
try {
|
|
102
|
+
return opts.retry(err, retryCount, opts)
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return Promise.reject(err)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof opts.retry === 'number') {
|
|
109
|
+
opts = { count: opts.retry }
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
const retryMax = opts.retry?.count ?? opts.maxRetries ?? 8
|
|
@@ -92,8 +115,10 @@ export function retry(err, retryCount, opts) {
|
|
|
92
115
|
return null
|
|
93
116
|
}
|
|
94
117
|
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
const statusCode = err.statusCode ?? err.status ?? err.$metadata?.httpStatusCode ?? null
|
|
119
|
+
|
|
120
|
+
if (statusCode && [420, 429, 502, 503, 504].includes(statusCode)) {
|
|
121
|
+
let retryAfter = err.headers?.['retry-after'] ? err.headers['retry-after'] * 1e3 : null
|
|
97
122
|
retryAfter = Number.isFinite(retryAfter) ? retryAfter : Math.min(10e3, retryCount * 1e3)
|
|
98
123
|
if (retryAfter != null && Number.isFinite(retryAfter)) {
|
|
99
124
|
return tp.setTimeout(retryAfter, undefined, { signal: opts.signal })
|
|
@@ -199,7 +224,7 @@ export function parseOrigin(url) {
|
|
|
199
224
|
}
|
|
200
225
|
|
|
201
226
|
export function parseHeaders(headers, obj = {}) {
|
|
202
|
-
return util.parseHeaders(headers, obj)
|
|
227
|
+
return Array.isArray(headers) ? util.parseHeaders(headers, obj) : headers
|
|
203
228
|
}
|
|
204
229
|
|
|
205
230
|
export class AbortError extends Error {
|
package/package.json
CHANGED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert'
|
|
2
|
-
import { parseContentRange, isDisturbed, findHeader, retry as retryFn } from '../utils.js'
|
|
3
|
-
|
|
4
|
-
class Handler {
|
|
5
|
-
constructor(opts, { dispatch, handler }) {
|
|
6
|
-
this.dispatch = dispatch
|
|
7
|
-
this.handler = handler
|
|
8
|
-
this.opts = opts
|
|
9
|
-
|
|
10
|
-
this.count = 0
|
|
11
|
-
this.pos = 0
|
|
12
|
-
this.end = null
|
|
13
|
-
this.error = null
|
|
14
|
-
this.etag = null
|
|
15
|
-
|
|
16
|
-
this.headersSent = false
|
|
17
|
-
|
|
18
|
-
this.reason = null
|
|
19
|
-
this.aborted = false
|
|
20
|
-
|
|
21
|
-
this.handler.onConnect((reason) => {
|
|
22
|
-
this.aborted = true
|
|
23
|
-
if (this.abort) {
|
|
24
|
-
this.abort(reason)
|
|
25
|
-
} else {
|
|
26
|
-
this.reason = reason
|
|
27
|
-
}
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
onConnect(abort) {
|
|
32
|
-
if (this.aborted) {
|
|
33
|
-
abort(this.reason)
|
|
34
|
-
} else {
|
|
35
|
-
this.abort = abort
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
onUpgrade(statusCode, rawHeaders, socket, headers) {
|
|
40
|
-
return this.handler.onUpgrade(statusCode, rawHeaders, socket, headers)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
|
|
44
|
-
const etag = headers ? headers.etag : findHeader(rawHeaders, 'etag')
|
|
45
|
-
|
|
46
|
-
if (this.resume) {
|
|
47
|
-
this.resume = null
|
|
48
|
-
|
|
49
|
-
// TODO (fix): Support other statusCode with skip?
|
|
50
|
-
if (statusCode !== 206) {
|
|
51
|
-
throw this.error
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// TODO (fix): strict vs weak etag?
|
|
55
|
-
if (this.etag == null || this.etag !== etag) {
|
|
56
|
-
throw this.error
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const contentRange = parseContentRange(
|
|
60
|
-
headers ? headers['content-range'] : findHeader(rawHeaders, 'content-range'),
|
|
61
|
-
)
|
|
62
|
-
if (!contentRange) {
|
|
63
|
-
throw this.error
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const { start, size, end = size } = contentRange
|
|
67
|
-
|
|
68
|
-
assert(this.pos === start, 'content-range mismatch')
|
|
69
|
-
assert(this.end == null || this.end === end, 'content-range mismatch')
|
|
70
|
-
|
|
71
|
-
this.resume = resume
|
|
72
|
-
return true
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (this.end == null) {
|
|
76
|
-
if (statusCode === 206) {
|
|
77
|
-
const contentRange = parseContentRange(
|
|
78
|
-
headers ? headers['content-range'] : findHeader(rawHeaders, 'content-range'),
|
|
79
|
-
)
|
|
80
|
-
if (!contentRange) {
|
|
81
|
-
assert(!this.headersSent)
|
|
82
|
-
this.headersSent = true
|
|
83
|
-
return this.handler.onHeaders(
|
|
84
|
-
statusCode,
|
|
85
|
-
rawHeaders,
|
|
86
|
-
() => this.resume(),
|
|
87
|
-
statusMessage,
|
|
88
|
-
headers,
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const { start, size, end = size } = contentRange
|
|
93
|
-
|
|
94
|
-
this.end = end
|
|
95
|
-
this.pos = Number(start)
|
|
96
|
-
} else {
|
|
97
|
-
const contentLength = headers
|
|
98
|
-
? headers['content-length']
|
|
99
|
-
: findHeader(rawHeaders, 'content-length')
|
|
100
|
-
if (contentLength) {
|
|
101
|
-
this.end = Number(contentLength)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
assert(Number.isFinite(this.pos))
|
|
106
|
-
assert(this.end == null || Number.isFinite(this.end), 'invalid content-length')
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
this.etag = etag
|
|
110
|
-
this.resume = resume
|
|
111
|
-
|
|
112
|
-
assert(!this.headersSent)
|
|
113
|
-
this.headersSent = true
|
|
114
|
-
return this.handler.onHeaders(
|
|
115
|
-
statusCode,
|
|
116
|
-
rawHeaders,
|
|
117
|
-
() => this.resume(),
|
|
118
|
-
statusMessage,
|
|
119
|
-
headers,
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
onData(chunk) {
|
|
124
|
-
this.pos += chunk.length
|
|
125
|
-
this.count = 0
|
|
126
|
-
return this.handler.onData(chunk)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
onComplete(rawTrailers) {
|
|
130
|
-
return this.handler.onComplete(rawTrailers)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
onError(err) {
|
|
134
|
-
if (this.aborted || !this.etag || isDisturbed(this.opts.body)) {
|
|
135
|
-
return this.handler.onError(err)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const retryPromise = retryFn(err, this.count++, this.opts)
|
|
139
|
-
if (retryPromise == null) {
|
|
140
|
-
return this.handler.onError(err)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
this.error = err
|
|
144
|
-
this.opts = {
|
|
145
|
-
...this.opts,
|
|
146
|
-
headers: {
|
|
147
|
-
...this.opts.headers,
|
|
148
|
-
'if-match': this.etag,
|
|
149
|
-
range: `bytes=${this.pos}-${this.end ?? ''}`,
|
|
150
|
-
},
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
this.opts.logger?.debug({ retryCount: this.count }, 'retrying response body')
|
|
154
|
-
|
|
155
|
-
retryPromise
|
|
156
|
-
.then(() => {
|
|
157
|
-
if (!this.aborted) {
|
|
158
|
-
this.dispatch(this.opts, this)
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
.catch((err) => {
|
|
162
|
-
if (!this.aborted) {
|
|
163
|
-
this.handler.onError(err)
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export default (dispatch) => (opts, handler) => {
|
|
170
|
-
return opts.idempotent && opts.retry && opts.method === 'GET' && !opts.upgrade
|
|
171
|
-
? dispatch(opts, new Handler(opts, { handler, dispatch }))
|
|
172
|
-
: dispatch(opts, handler)
|
|
173
|
-
}
|
|
174
|
-
export function isConnectionError(err) {
|
|
175
|
-
// AWS compat.
|
|
176
|
-
const statusCode = err?.statusCode ?? err?.$metadata?.httpStatusCode
|
|
177
|
-
return err
|
|
178
|
-
? err.code === 'ECONNRESET' ||
|
|
179
|
-
err.code === 'ECONNREFUSED' ||
|
|
180
|
-
err.code === 'ENOTFOUND' ||
|
|
181
|
-
err.code === 'ENETDOWN' ||
|
|
182
|
-
err.code === 'ENETUNREACH' ||
|
|
183
|
-
err.code === 'EHOSTDOWN' ||
|
|
184
|
-
err.code === 'EHOSTUNREACH' ||
|
|
185
|
-
err.code === 'EPIPE' ||
|
|
186
|
-
err.message === 'other side closed' ||
|
|
187
|
-
statusCode === 420 ||
|
|
188
|
-
statusCode === 429 ||
|
|
189
|
-
statusCode === 502 ||
|
|
190
|
-
statusCode === 503 ||
|
|
191
|
-
statusCode === 504
|
|
192
|
-
: false
|
|
193
|
-
}
|