@nxtedition/nxt-undici 2.0.14 → 2.0.16
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 +46 -32
- package/lib/interceptor/cache.js +5 -14
- package/lib/interceptor/log.js +3 -3
- package/lib/interceptor/redirect.js +50 -41
- package/lib/interceptor/request-body-factory.js +1 -1
- package/lib/interceptor/request-body.js +2 -2
- package/lib/interceptor/request-content.js +2 -2
- package/lib/interceptor/response-content.js +2 -2
- package/lib/interceptor/response-error.js +89 -0
- package/lib/interceptor/response-retry.js +107 -35
- package/lib/utils.js +28 -6
- package/package.json +1 -1
- package/lib/interceptor/response-body-retry.js +0 -162
- package/lib/interceptor/response-status-retry.js +0 -104
- package/lib/interceptor/signal.js +0 -57
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import assert from 'node:assert'
|
|
|
2
2
|
import stream from 'node:stream'
|
|
3
3
|
import createError from 'http-errors'
|
|
4
4
|
import undici from 'undici'
|
|
5
|
-
import { parseHeaders, AbortError } from './utils.js'
|
|
5
|
+
import { findHeader, parseHeaders, AbortError } from './utils.js'
|
|
6
6
|
import CacheableLookup from 'cacheable-lookup'
|
|
7
7
|
|
|
8
8
|
const dispatcherCache = new WeakMap()
|
|
@@ -25,13 +25,15 @@ const kStatusCode = Symbol('statusCode')
|
|
|
25
25
|
const kStatusMessage = Symbol('statusMessage')
|
|
26
26
|
const kHeaders = Symbol('headers')
|
|
27
27
|
const kSize = Symbol('size')
|
|
28
|
+
const kHandler = Symbol('handler')
|
|
28
29
|
|
|
29
30
|
let ABORT_ERROR
|
|
30
31
|
|
|
31
32
|
class Readable extends stream.Readable {
|
|
32
|
-
constructor({ statusCode, statusMessage, headers, size, abort, highWaterMark, resume }) {
|
|
33
|
-
super(
|
|
33
|
+
constructor(handler, { statusCode, statusMessage, headers, size, abort, highWaterMark, resume }) {
|
|
34
|
+
super({ highWaterMark })
|
|
34
35
|
|
|
36
|
+
this[kHandler] = handler
|
|
35
37
|
this[kStatusCode] = statusCode
|
|
36
38
|
this[kStatusMessage] = statusMessage
|
|
37
39
|
this[kHeaders] = headers
|
|
@@ -71,6 +73,11 @@ class Readable extends stream.Readable {
|
|
|
71
73
|
this[kAbort](err)
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
if (this[kHandler].signal) {
|
|
77
|
+
this[kHandler].signal.removeEventListener('abort', this[kHandler].onAbort)
|
|
78
|
+
this[kHandler].signal = null
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
callback(err)
|
|
75
82
|
}
|
|
76
83
|
|
|
@@ -127,16 +134,14 @@ class Readable extends stream.Readable {
|
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
const dispatchers = {
|
|
137
|
+
responseError: (await import('./interceptor/response-error.js')).default,
|
|
130
138
|
requestBody: (await import('./interceptor/request-body.js')).default,
|
|
131
139
|
requestBodyFactory: (await import('./interceptor/request-body-factory.js')).default,
|
|
132
140
|
responseContent: (await import('./interceptor/response-content.js')).default,
|
|
133
141
|
requestContent: (await import('./interceptor/request-content.js')).default,
|
|
134
142
|
log: (await import('./interceptor/log.js')).default,
|
|
135
143
|
redirect: (await import('./interceptor/redirect.js')).default,
|
|
136
|
-
responseBodyRetry: (await import('./interceptor/response-body-retry.js')).default,
|
|
137
|
-
responseStatusRetry: (await import('./interceptor/response-status-retry.js')).default,
|
|
138
144
|
responseRetry: (await import('./interceptor/response-retry.js')).default,
|
|
139
|
-
signal: (await import('./interceptor/signal.js')).default,
|
|
140
145
|
proxy: (await import('./interceptor/proxy.js')).default,
|
|
141
146
|
cache: (await import('./interceptor/cache.js')).default,
|
|
142
147
|
requestId: (await import('./interceptor/request-id.js')).default,
|
|
@@ -222,23 +227,21 @@ export async function request(url, opts) {
|
|
|
222
227
|
let dispatch = dispatcherCache.get(dispatcher)
|
|
223
228
|
if (dispatch == null) {
|
|
224
229
|
dispatch = (opts, handler) => dispatcher.dispatch(opts, handler)
|
|
230
|
+
dispatch = dispatchers.responseError(dispatch)
|
|
225
231
|
dispatch = dispatchers.requestBodyFactory(dispatch)
|
|
226
232
|
dispatch = dispatchers.log(dispatch)
|
|
227
233
|
dispatch = dispatchers.requestId(dispatch)
|
|
228
234
|
dispatch = dispatchers.responseRetry(dispatch)
|
|
229
|
-
dispatch = dispatchers.responseStatusRetry(dispatch)
|
|
230
|
-
dispatch = dispatchers.responseBodyRetry(dispatch)
|
|
231
235
|
dispatch = dispatchers.responseContent(dispatch)
|
|
232
236
|
dispatch = dispatchers.requestContent(dispatch)
|
|
233
237
|
dispatch = dispatchers.redirect(dispatch)
|
|
234
|
-
dispatch = dispatchers.signal(dispatch)
|
|
235
238
|
dispatch = dispatchers.cache(dispatch)
|
|
236
239
|
dispatch = dispatchers.proxy(dispatch)
|
|
237
240
|
dispatch = dispatchers.requestBody(dispatch)
|
|
238
241
|
dispatcherCache.set(dispatcher, dispatch)
|
|
239
242
|
}
|
|
240
243
|
|
|
241
|
-
|
|
244
|
+
return await new Promise((resolve, reject) =>
|
|
242
245
|
dispatch(
|
|
243
246
|
{
|
|
244
247
|
id: opts.id ?? headers?.['request-id'] ?? headers?.['Request-Id'] ?? genReqId(),
|
|
@@ -253,7 +256,6 @@ export async function request(url, opts) {
|
|
|
253
256
|
headersTimeout: opts.headersTimeout,
|
|
254
257
|
bodyTimeout: opts.bodyTimeout,
|
|
255
258
|
idempotent,
|
|
256
|
-
signal: opts.signal,
|
|
257
259
|
retry: opts.retry ?? 8,
|
|
258
260
|
proxy: opts.proxy,
|
|
259
261
|
cache: opts.cache,
|
|
@@ -266,10 +268,26 @@ export async function request(url, opts) {
|
|
|
266
268
|
resolve,
|
|
267
269
|
reject,
|
|
268
270
|
logger: opts.logger,
|
|
271
|
+
signal: opts.signal,
|
|
269
272
|
/** @type {Function | null} */ abort: null,
|
|
270
273
|
/** @type {stream.Readable | null} */ body: null,
|
|
271
274
|
onConnect(abort) {
|
|
272
|
-
this.
|
|
275
|
+
if (this.signal?.aborted) {
|
|
276
|
+
abort(this.signal.reason)
|
|
277
|
+
} else {
|
|
278
|
+
this.abort = abort
|
|
279
|
+
|
|
280
|
+
if (this.signal) {
|
|
281
|
+
this.onAbort = () => {
|
|
282
|
+
if (this.body) {
|
|
283
|
+
this.body.destroy(this.signal.reason ?? new AbortError())
|
|
284
|
+
} else {
|
|
285
|
+
this.abort(this.signal.reason)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.signal.addEventListener('abort', this.onAbort)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
273
291
|
},
|
|
274
292
|
onUpgrade(statusCode, rawHeaders, socket) {
|
|
275
293
|
const headers = parseHeaders(rawHeaders)
|
|
@@ -283,14 +301,18 @@ export async function request(url, opts) {
|
|
|
283
301
|
},
|
|
284
302
|
onBodySent(chunk) {},
|
|
285
303
|
onRequestSent() {},
|
|
286
|
-
onHeaders(
|
|
287
|
-
|
|
288
|
-
|
|
304
|
+
onHeaders(
|
|
305
|
+
statusCode,
|
|
306
|
+
rawHeaders,
|
|
307
|
+
resume,
|
|
308
|
+
statusMessage,
|
|
309
|
+
headers = parseHeaders(rawHeaders),
|
|
310
|
+
) {
|
|
289
311
|
assert(statusCode >= 200)
|
|
290
312
|
|
|
291
|
-
const contentLength =
|
|
313
|
+
const contentLength = findHeader(rawHeaders, 'content-length')
|
|
292
314
|
|
|
293
|
-
this.body = new Readable({
|
|
315
|
+
this.body = new Readable(this, {
|
|
294
316
|
resume,
|
|
295
317
|
abort: this.abort,
|
|
296
318
|
highWaterMark: this.highWaterMark,
|
|
@@ -304,15 +326,21 @@ export async function request(url, opts) {
|
|
|
304
326
|
this.resolve = null
|
|
305
327
|
this.reject = null
|
|
306
328
|
|
|
307
|
-
return
|
|
329
|
+
return true
|
|
308
330
|
},
|
|
309
331
|
onData(chunk) {
|
|
310
332
|
return this.body.push(chunk)
|
|
311
333
|
},
|
|
312
334
|
onComplete() {
|
|
335
|
+
this.signal?.removeEventListener('abort', this.onAbort)
|
|
336
|
+
this.signal = null
|
|
337
|
+
|
|
313
338
|
this.body.push(null)
|
|
314
339
|
},
|
|
315
340
|
onError(err) {
|
|
341
|
+
this.signal?.removeEventListener('abort', this.onAbort)
|
|
342
|
+
this.signal = null
|
|
343
|
+
|
|
316
344
|
if (this.body) {
|
|
317
345
|
this.body.destroy(err)
|
|
318
346
|
} else {
|
|
@@ -324,18 +352,4 @@ export async function request(url, opts) {
|
|
|
324
352
|
},
|
|
325
353
|
),
|
|
326
354
|
)
|
|
327
|
-
|
|
328
|
-
if (method === 'HEAD') {
|
|
329
|
-
await res.dump()
|
|
330
|
-
} else if (res.statusCode >= 400) {
|
|
331
|
-
// TODO (fix): Limit the size of the body?
|
|
332
|
-
const data = /^application\/json$/i.test(res.headers['content-type'])
|
|
333
|
-
? await res.json()
|
|
334
|
-
: /^text-.+/i.test(res.headers['content-type'])
|
|
335
|
-
? await res.text()
|
|
336
|
-
: await res.dump()
|
|
337
|
-
throw createError(res.statusCode, { headers, data })
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return res
|
|
341
355
|
}
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
import { LRUCache } from 'lru-cache'
|
|
3
|
-
import
|
|
3
|
+
import { findHeader, parseCacheControl } from '../utils.js'
|
|
4
4
|
|
|
5
5
|
class CacheHandler {
|
|
6
6
|
constructor({ key, handler, store }) {
|
|
@@ -26,22 +26,13 @@ class CacheHandler {
|
|
|
26
26
|
return this.handler.onRequestSent()
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
29
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
|
|
30
30
|
// NOTE: Only cache 307 respones for now...
|
|
31
31
|
if (statusCode !== 307) {
|
|
32
|
-
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
32
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
for (let n = 0; n < rawHeaders.length; n += 2) {
|
|
37
|
-
if (
|
|
38
|
-
rawHeaders[n].length === 'cache-control'.length &&
|
|
39
|
-
rawHeaders[n].toString().toLowerCase() === 'cache-control'
|
|
40
|
-
) {
|
|
41
|
-
cacheControl = cacheControlParser.parse(rawHeaders[n + 1].toString())
|
|
42
|
-
break
|
|
43
|
-
}
|
|
44
|
-
}
|
|
35
|
+
const cacheControl = parseCacheControl(findHeader(rawHeaders, 'cache-control'))
|
|
45
36
|
|
|
46
37
|
if (
|
|
47
38
|
cacheControl &&
|
|
@@ -73,7 +64,7 @@ class CacheHandler {
|
|
|
73
64
|
}
|
|
74
65
|
}
|
|
75
66
|
|
|
76
|
-
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
67
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
77
68
|
}
|
|
78
69
|
|
|
79
70
|
onData(chunk) {
|
package/lib/interceptor/log.js
CHANGED
|
@@ -54,18 +54,18 @@ class Handler {
|
|
|
54
54
|
return this.handler.onRequestSent()
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
57
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
|
|
58
58
|
this.stats.headers = performance.now() - this.stats.start
|
|
59
59
|
|
|
60
60
|
this.logger.debug(
|
|
61
61
|
{
|
|
62
|
-
ures: { statusCode, headers
|
|
62
|
+
ures: { statusCode, headers },
|
|
63
63
|
elapsedTime: this.stats.headers,
|
|
64
64
|
},
|
|
65
65
|
'upstream request response',
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
68
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
onData(chunk) {
|
|
@@ -4,16 +4,34 @@ import { findHeader, isDisturbed, parseURL } from '../utils.js'
|
|
|
4
4
|
const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
|
|
5
5
|
|
|
6
6
|
class Handler {
|
|
7
|
-
constructor(opts, { dispatch, handler
|
|
7
|
+
constructor(opts, { dispatch, handler }) {
|
|
8
8
|
this.dispatch = dispatch
|
|
9
9
|
this.handler = handler
|
|
10
10
|
this.opts = opts
|
|
11
|
-
this.
|
|
12
|
-
this.
|
|
11
|
+
this.abort = null
|
|
12
|
+
this.aborted = false
|
|
13
|
+
this.reason = null
|
|
14
|
+
this.maxCount = Number.isFinite(opts.follow) ? opts.follow : opts.follow?.count ?? 0
|
|
15
|
+
|
|
16
|
+
this.count = 0
|
|
17
|
+
this.location = 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
|
+
})
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
onConnect(abort) {
|
|
16
|
-
|
|
30
|
+
if (this.aborted) {
|
|
31
|
+
abort(this.reason)
|
|
32
|
+
} else {
|
|
33
|
+
this.abort = abort
|
|
34
|
+
}
|
|
17
35
|
}
|
|
18
36
|
|
|
19
37
|
onUpgrade(statusCode, headers, socket) {
|
|
@@ -28,47 +46,42 @@ class Handler {
|
|
|
28
46
|
return this.handler.onRequestSent()
|
|
29
47
|
}
|
|
30
48
|
|
|
31
|
-
onHeaders(statusCode,
|
|
49
|
+
onHeaders(statusCode, rawHeaders, resume, statusText, headers) {
|
|
32
50
|
if (redirectableStatusCodes.indexOf(statusCode) === -1) {
|
|
33
|
-
return this.handler.onHeaders(statusCode,
|
|
51
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusText, headers)
|
|
34
52
|
}
|
|
35
53
|
|
|
36
|
-
|
|
54
|
+
if (isDisturbed(this.opts.body)) {
|
|
55
|
+
throw new Error(`Disturbed request cannot be redirected.`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.location = findHeader(rawHeaders, 'location')
|
|
37
59
|
|
|
38
|
-
if (!location) {
|
|
39
|
-
// TODO (perf): Consume body?
|
|
60
|
+
if (!this.location) {
|
|
40
61
|
throw new Error(`Missing redirection location .`)
|
|
41
62
|
}
|
|
42
63
|
|
|
43
64
|
this.count += 1
|
|
44
65
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
:
|
|
52
|
-
|
|
53
|
-
if (this.count >= maxCount) {
|
|
54
|
-
// TODO (perf): Consume body?
|
|
55
|
-
throw new Error(`Max redirections reached: ${maxCount}.`)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (isDisturbed(this.opts.body)) {
|
|
59
|
-
// TODO (perf): Consume body?
|
|
60
|
-
throw new Error(`Disturbed request cannot be redirected.`)
|
|
66
|
+
if (typeof this.opts.follow === 'function') {
|
|
67
|
+
if (!this.opts.follow(this.location, this.count)) {
|
|
68
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusText, headers)
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
if (this.count >= this.maxCount) {
|
|
72
|
+
throw new Error(`Max redirections reached: ${this.maxCount}.`)
|
|
73
|
+
}
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
const { origin, pathname, search } = parseURL(
|
|
64
|
-
new URL(location, this.opts.origin && new URL(this.opts.path, this.opts.origin)),
|
|
77
|
+
new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)),
|
|
65
78
|
)
|
|
66
79
|
const path = search ? `${pathname}${search}` : pathname
|
|
67
80
|
|
|
68
81
|
// Remove headers referring to the original URL.
|
|
69
82
|
// By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
|
|
70
83
|
// https://tools.ietf.org/html/rfc7231#section-6.4
|
|
71
|
-
this.
|
|
84
|
+
this.opts = {
|
|
72
85
|
...this.opts,
|
|
73
86
|
headers: cleanRequestHeaders(
|
|
74
87
|
this.opts.headers,
|
|
@@ -77,17 +90,18 @@ class Handler {
|
|
|
77
90
|
),
|
|
78
91
|
path,
|
|
79
92
|
origin,
|
|
93
|
+
query: null,
|
|
80
94
|
}
|
|
81
95
|
|
|
82
96
|
// https://tools.ietf.org/html/rfc7231#section-6.4.4
|
|
83
97
|
// In case of HTTP 303, always replace method to be either HEAD or GET
|
|
84
|
-
if (statusCode === 303 && this.
|
|
85
|
-
this.
|
|
98
|
+
if (statusCode === 303 && this.opts.method !== 'HEAD') {
|
|
99
|
+
this.opts = { ...this.opts, method: 'GET', body: null }
|
|
86
100
|
}
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
onData(chunk) {
|
|
90
|
-
if (this.
|
|
104
|
+
if (this.location) {
|
|
91
105
|
/*
|
|
92
106
|
https://tools.ietf.org/html/rfc7231#section-6.4
|
|
93
107
|
|
|
@@ -111,7 +125,7 @@ class Handler {
|
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
onComplete(trailers) {
|
|
114
|
-
if (this.
|
|
128
|
+
if (this.location) {
|
|
115
129
|
/*
|
|
116
130
|
https://tools.ietf.org/html/rfc7231#section-6.4
|
|
117
131
|
|
|
@@ -120,15 +134,10 @@ class Handler {
|
|
|
120
134
|
|
|
121
135
|
See comment on onData method above for more detailed informations.
|
|
122
136
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
dispatch: this.dispatch,
|
|
128
|
-
count: this.count,
|
|
129
|
-
}),
|
|
130
|
-
)
|
|
131
|
-
this.handler = null
|
|
137
|
+
|
|
138
|
+
this.location = null
|
|
139
|
+
|
|
140
|
+
this.dispatch(this.opts, this)
|
|
132
141
|
} else {
|
|
133
142
|
return this.handler.onComplete(trailers)
|
|
134
143
|
}
|
|
@@ -169,5 +178,5 @@ function cleanRequestHeaders(headers, removeContent, unknownOrigin) {
|
|
|
169
178
|
|
|
170
179
|
export default (dispatch) => (opts, handler) =>
|
|
171
180
|
opts.follow != null
|
|
172
|
-
? dispatch(opts, new Handler(opts, { handler, dispatch
|
|
181
|
+
? dispatch(opts, new Handler(opts, { handler, dispatch }))
|
|
173
182
|
: dispatch(opts, handler)
|
|
@@ -8,7 +8,7 @@ export default (dispatch) => (opts, handler) => {
|
|
|
8
8
|
|
|
9
9
|
const body = opts.body({ signal: opts.signal })
|
|
10
10
|
|
|
11
|
-
if (typeof body
|
|
11
|
+
if (typeof body?.then === 'function') {
|
|
12
12
|
body.then(
|
|
13
13
|
(body) => dispatch({ ...opts, body }, handler),
|
|
14
14
|
(err) => handler.onError(err),
|
|
@@ -39,8 +39,8 @@ class Handler {
|
|
|
39
39
|
return this.handler.onRequestSent()
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
43
|
-
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
42
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
|
|
43
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
onData(chunk) {
|
|
@@ -51,8 +51,8 @@ class Handler {
|
|
|
51
51
|
return this.handler.onRequestSent()
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
55
|
-
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
54
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
|
|
55
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
onData(chunk) {
|
|
@@ -26,12 +26,12 @@ class Handler {
|
|
|
26
26
|
return this.handler.onRequestSent()
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
29
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
|
|
30
30
|
this.md5 = findHeader(rawHeaders, 'content-md5')
|
|
31
31
|
this.length = findHeader(rawHeaders, 'content-length')
|
|
32
32
|
this.hasher = this.md5 != null ? crypto.createHash('md5') : null
|
|
33
33
|
|
|
34
|
-
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
34
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
onData(chunk) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { findHeader, parseHeaders } from '../utils.js'
|
|
2
|
+
import createHttpError from 'http-errors'
|
|
3
|
+
|
|
4
|
+
class Handler {
|
|
5
|
+
constructor(opts, { handler }) {
|
|
6
|
+
this.handler = handler
|
|
7
|
+
this.statusCode = 0
|
|
8
|
+
this.contentType = null
|
|
9
|
+
this.decoder = null
|
|
10
|
+
this.headers = null
|
|
11
|
+
this.body = null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
onConnect(abort) {
|
|
15
|
+
return this.handler.onConnect(abort)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
onUpgrade(statusCode, rawHeaders, socket) {
|
|
19
|
+
return this.handler.onUpgrade(statusCode, rawHeaders, socket)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onBodySent(chunk) {
|
|
23
|
+
return this.handler.onBodySent(chunk)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onRequestSent() {
|
|
27
|
+
return this.handler.onRequestSent()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
|
|
31
|
+
if (statusCode >= 400) {
|
|
32
|
+
this.statusCode = statusCode
|
|
33
|
+
this.headers = headers
|
|
34
|
+
this.contentType = findHeader(rawHeaders, 'content-type')
|
|
35
|
+
if (this.contentType === 'application/json' || this.contentType === 'text/plain') {
|
|
36
|
+
this.decoder = new TextDecoder('utf-8')
|
|
37
|
+
this.body = ''
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onData(chunk) {
|
|
45
|
+
if (this.statusCode) {
|
|
46
|
+
if (this.decoder) {
|
|
47
|
+
// TODO (fix): Limit body size?
|
|
48
|
+
this.body += this.decoder.decode(chunk, { stream: true })
|
|
49
|
+
}
|
|
50
|
+
return true
|
|
51
|
+
} else {
|
|
52
|
+
return this.handler.onData(chunk)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onComplete(rawTrailers) {
|
|
57
|
+
this.onFinally(null, rawTrailers)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
onError(err) {
|
|
61
|
+
this.onFinally(err, null)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onFinally(err, rawTrailers) {
|
|
65
|
+
if (this.statusCode) {
|
|
66
|
+
const stackTraceLimit = Error.stackTraceLimit
|
|
67
|
+
Error.stackTraceLimit = 0
|
|
68
|
+
try {
|
|
69
|
+
if (this.decoder != null) {
|
|
70
|
+
this.body += this.decoder.decode(undefined, { stream: false })
|
|
71
|
+
if (this.contentType === 'application/json') {
|
|
72
|
+
this.body = JSON.parse(this.body)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.handler.onError(
|
|
76
|
+
createHttpError(this.statusCode, { headers: this.headers, body: this.body }),
|
|
77
|
+
)
|
|
78
|
+
} finally {
|
|
79
|
+
Error.stackTraceLimit = stackTraceLimit
|
|
80
|
+
}
|
|
81
|
+
} else if (err) {
|
|
82
|
+
this.handler.onError(err)
|
|
83
|
+
} else {
|
|
84
|
+
this.handler.onComplete(rawTrailers)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default (dispatch) => (opts, handler) => dispatch(opts, new Handler(opts, { handler }))
|
|
@@ -1,34 +1,27 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { parseContentRange, isDisturbed, findHeader, retry as retryFn } from '../utils.js'
|
|
2
3
|
|
|
3
4
|
class Handler {
|
|
4
5
|
constructor(opts, { dispatch, handler }) {
|
|
5
6
|
this.dispatch = dispatch
|
|
6
7
|
this.handler = handler
|
|
7
8
|
this.opts = opts
|
|
8
|
-
this.abort = null
|
|
9
|
-
this.aborted = false
|
|
10
|
-
this.reason = null
|
|
11
|
-
this.statusCode = 0
|
|
12
|
-
|
|
13
|
-
this.retryCount = 0
|
|
14
|
-
this.retryPromise = null
|
|
15
9
|
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
})
|
|
10
|
+
this.hasBody = false
|
|
11
|
+
this.aborted = false
|
|
12
|
+
this.count = 0
|
|
13
|
+
this.pos = 0
|
|
14
|
+
this.end = null
|
|
15
|
+
this.error = null
|
|
16
|
+
this.etag = null
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
onConnect(abort) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
20
|
+
this.aborted = false
|
|
21
|
+
return this.handler.onConnect((reason) => {
|
|
22
|
+
this.aborted = true
|
|
23
|
+
abort(reason)
|
|
24
|
+
})
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
onUpgrade(statusCode, rawHeaders, socket) {
|
|
@@ -43,12 +36,81 @@ class Handler {
|
|
|
43
36
|
return this.handler.onRequestSent()
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
|
|
40
|
+
const etag = findHeader(rawHeaders, 'etag')
|
|
41
|
+
|
|
42
|
+
if (this.resume) {
|
|
43
|
+
this.resume = null
|
|
44
|
+
|
|
45
|
+
// TODO (fix): Support other statusCode with skip?
|
|
46
|
+
if (statusCode !== 206) {
|
|
47
|
+
throw this.error
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// TODO (fix): strict vs weak etag?
|
|
51
|
+
if (this.etag == null || this.etag !== etag) {
|
|
52
|
+
throw this.error
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
|
|
56
|
+
if (!contentRange) {
|
|
57
|
+
throw this.error
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { start, size, end = size } = contentRange
|
|
61
|
+
|
|
62
|
+
assert(this.pos === start, 'content-range mismatch')
|
|
63
|
+
assert(this.end == null || this.end === end, 'content-range mismatch')
|
|
64
|
+
|
|
65
|
+
this.resume = resume
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (this.end == null) {
|
|
70
|
+
if (statusCode === 206) {
|
|
71
|
+
const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
|
|
72
|
+
if (!contentRange) {
|
|
73
|
+
return this.handler.onHeaders(
|
|
74
|
+
statusCode,
|
|
75
|
+
rawHeaders,
|
|
76
|
+
() => this.resume(),
|
|
77
|
+
statusMessage,
|
|
78
|
+
headers,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { start, size, end = size } = contentRange
|
|
83
|
+
|
|
84
|
+
this.end = end
|
|
85
|
+
this.pos = Number(start)
|
|
86
|
+
} else {
|
|
87
|
+
const contentLength = findHeader(rawHeaders, 'content-length')
|
|
88
|
+
if (contentLength) {
|
|
89
|
+
this.end = Number(contentLength)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
assert(Number.isFinite(this.pos))
|
|
94
|
+
assert(this.end == null || Number.isFinite(this.end), 'invalid content-length')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.etag = etag
|
|
98
|
+
this.resume = resume
|
|
99
|
+
|
|
100
|
+
return this.handler.onHeaders(
|
|
101
|
+
statusCode,
|
|
102
|
+
rawHeaders,
|
|
103
|
+
() => this.resume(),
|
|
104
|
+
statusMessage,
|
|
105
|
+
headers,
|
|
106
|
+
)
|
|
49
107
|
}
|
|
50
108
|
|
|
51
109
|
onData(chunk) {
|
|
110
|
+
this.pos += chunk.length
|
|
111
|
+
this.count = 0
|
|
112
|
+
this.hasBody = true
|
|
113
|
+
|
|
52
114
|
return this.handler.onData(chunk)
|
|
53
115
|
}
|
|
54
116
|
|
|
@@ -57,23 +119,34 @@ class Handler {
|
|
|
57
119
|
}
|
|
58
120
|
|
|
59
121
|
onError(err) {
|
|
60
|
-
if (this.aborted || this.
|
|
122
|
+
if (this.aborted || (this.hasBody && !this.etag) || isDisturbed(this.opts.body)) {
|
|
61
123
|
return this.handler.onError(err)
|
|
62
124
|
}
|
|
63
125
|
|
|
64
|
-
const retryPromise = retryFn(err, this.
|
|
126
|
+
const retryPromise = retryFn(err, this.count++, this.opts)
|
|
65
127
|
if (retryPromise == null) {
|
|
66
128
|
return this.handler.onError(err)
|
|
67
129
|
}
|
|
68
130
|
|
|
131
|
+
this.error = err
|
|
132
|
+
|
|
133
|
+
if (this.hasBody) {
|
|
134
|
+
this.opts = {
|
|
135
|
+
...this.opts,
|
|
136
|
+
headers: {
|
|
137
|
+
...this.opts.headers,
|
|
138
|
+
'if-match': this.etag,
|
|
139
|
+
range: `bytes=${this.pos}-${this.end ?? ''}`,
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.opts.logger?.debug('retrying response body')
|
|
145
|
+
|
|
69
146
|
retryPromise
|
|
70
147
|
.then(() => {
|
|
71
148
|
if (!this.aborted) {
|
|
72
|
-
|
|
73
|
-
this.dispatch(this.opts, this)
|
|
74
|
-
} catch (err2) {
|
|
75
|
-
this.handler.onError(new AggregateError([err, err2]))
|
|
76
|
-
}
|
|
149
|
+
this.dispatch(this.opts, this)
|
|
77
150
|
}
|
|
78
151
|
})
|
|
79
152
|
.catch((err) => {
|
|
@@ -81,12 +154,11 @@ class Handler {
|
|
|
81
154
|
this.handler.onError(err)
|
|
82
155
|
}
|
|
83
156
|
})
|
|
84
|
-
|
|
85
|
-
this.opts.logger?.debug('retrying response')
|
|
86
157
|
}
|
|
87
158
|
}
|
|
88
159
|
|
|
89
|
-
export default (dispatch) => (opts, handler) =>
|
|
90
|
-
opts.idempotent && opts.retry && !opts.upgrade
|
|
160
|
+
export default (dispatch) => (opts, handler) => {
|
|
161
|
+
return opts.idempotent && opts.retry && opts.method === 'GET' && !opts.upgrade
|
|
91
162
|
? dispatch(opts, new Handler(opts, { handler, dispatch }))
|
|
92
163
|
: dispatch(opts, handler)
|
|
164
|
+
}
|
package/lib/utils.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import tp from 'node:timers/promises'
|
|
2
|
+
import cacheControlParser from 'cache-control-parser'
|
|
3
|
+
|
|
4
|
+
export function parseCacheControl(str) {
|
|
5
|
+
return str ? cacheControlParser.parse(str) : null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const lowerCaseCache = new Map()
|
|
9
|
+
export function toLowerCase(val) {
|
|
10
|
+
let ret = lowerCaseCache.get(val)
|
|
11
|
+
if (ret === undefined) {
|
|
12
|
+
ret = val.toLowerCase()
|
|
13
|
+
lowerCaseCache.set(val, ret)
|
|
14
|
+
}
|
|
15
|
+
return ret
|
|
16
|
+
}
|
|
2
17
|
|
|
3
18
|
export function isDisturbed(body) {
|
|
4
19
|
if (
|
|
@@ -50,10 +65,11 @@ export function findHeader(rawHeaders, name) {
|
|
|
50
65
|
|
|
51
66
|
for (let i = 0; i < rawHeaders.length; i += 2) {
|
|
52
67
|
const key = rawHeaders[i + 0]
|
|
53
|
-
if (key.length === len && key.toString()
|
|
68
|
+
if (key.length === len && toLowerCase(key.toString()) === name) {
|
|
54
69
|
return rawHeaders[i + 1].toString()
|
|
55
70
|
}
|
|
56
71
|
}
|
|
72
|
+
|
|
57
73
|
return null
|
|
58
74
|
}
|
|
59
75
|
|
|
@@ -178,18 +194,24 @@ export function parseOrigin(url) {
|
|
|
178
194
|
return url
|
|
179
195
|
}
|
|
180
196
|
|
|
181
|
-
export function parseHeaders(
|
|
182
|
-
for (let i = 0; i <
|
|
183
|
-
const key =
|
|
197
|
+
export function parseHeaders(headers, obj = {}) {
|
|
198
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
199
|
+
const key = toLowerCase(headers[i].toString())
|
|
200
|
+
|
|
184
201
|
let val = obj[key]
|
|
185
202
|
if (!val) {
|
|
186
|
-
|
|
203
|
+
val = headers[i + 1]
|
|
204
|
+
if (typeof val === 'string') {
|
|
205
|
+
obj[key] = val
|
|
206
|
+
} else {
|
|
207
|
+
obj[key] = Array.isArray(val) ? val.map((x) => x.toString()) : val.toString()
|
|
208
|
+
}
|
|
187
209
|
} else {
|
|
188
210
|
if (!Array.isArray(val)) {
|
|
189
211
|
val = [val]
|
|
190
212
|
obj[key] = val
|
|
191
213
|
}
|
|
192
|
-
val.push(
|
|
214
|
+
val.push(headers[i + 1].toString())
|
|
193
215
|
}
|
|
194
216
|
}
|
|
195
217
|
return obj
|
package/package.json
CHANGED
|
@@ -1,162 +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
|
-
this.abort = null
|
|
10
|
-
this.aborted = false
|
|
11
|
-
this.reason = null
|
|
12
|
-
|
|
13
|
-
this.retryCount = 0
|
|
14
|
-
this.retryPromise = null
|
|
15
|
-
|
|
16
|
-
this.pos = 0
|
|
17
|
-
this.end = null
|
|
18
|
-
this.error = null
|
|
19
|
-
this.etag = null
|
|
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) {
|
|
40
|
-
return this.handler.onUpgrade(statusCode, rawHeaders, socket)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
onBodySent(chunk) {
|
|
44
|
-
return this.handler.onBodySent(chunk)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
onRequestSent() {
|
|
48
|
-
return this.handler.onRequestSent()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
52
|
-
const etag = findHeader(rawHeaders, 'etag')
|
|
53
|
-
|
|
54
|
-
if (this.resume) {
|
|
55
|
-
this.resume = null
|
|
56
|
-
|
|
57
|
-
// TODO (fix): Support other statusCode with skip?
|
|
58
|
-
if (statusCode !== 206) {
|
|
59
|
-
throw this.error
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// TODO (fix): strict vs weak etag?
|
|
63
|
-
if (this.etag == null || this.etag !== etag) {
|
|
64
|
-
throw this.error
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
|
|
68
|
-
if (!contentRange) {
|
|
69
|
-
throw this.error
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const { start, size, end = size } = contentRange
|
|
73
|
-
|
|
74
|
-
assert(this.pos === start, 'content-range mismatch')
|
|
75
|
-
assert(this.end == null || this.end === end, 'content-range mismatch')
|
|
76
|
-
|
|
77
|
-
this.resume = resume
|
|
78
|
-
return true
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (this.end == null) {
|
|
82
|
-
if (statusCode === 206) {
|
|
83
|
-
const contentRange = parseContentRange(findHeader(rawHeaders, 'content-range'))
|
|
84
|
-
if (!contentRange) {
|
|
85
|
-
return this.handler.onHeaders(statusCode, rawHeaders, () => this.resume(), statusMessage)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const { start, size, end = size } = contentRange
|
|
89
|
-
|
|
90
|
-
this.end = end
|
|
91
|
-
this.pos = Number(start)
|
|
92
|
-
} else {
|
|
93
|
-
const contentLength = findHeader(rawHeaders, 'content-length')
|
|
94
|
-
if (contentLength) {
|
|
95
|
-
this.end = Number(contentLength)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
assert(Number.isFinite(this.pos))
|
|
100
|
-
assert(this.end == null || Number.isFinite(this.end), 'invalid content-length')
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
this.etag = etag
|
|
104
|
-
this.resume = resume
|
|
105
|
-
return this.handler.onHeaders(statusCode, rawHeaders, () => this.resume(), statusMessage)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
onData(chunk) {
|
|
109
|
-
this.pos += chunk.length
|
|
110
|
-
this.count = 0
|
|
111
|
-
return this.handler.onData(chunk)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
onComplete(rawTrailers) {
|
|
115
|
-
return this.handler.onComplete(rawTrailers)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
onError(err) {
|
|
119
|
-
if (!this.resume || this.aborted || !this.etag || isDisturbed(this.opts.body)) {
|
|
120
|
-
return this.handler.onError(err)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const retryPromise = retryFn(err, this.retryCount++, this.opts)
|
|
124
|
-
if (retryPromise == null) {
|
|
125
|
-
return this.handler.onError(err)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
retryPromise
|
|
129
|
-
.then(() => {
|
|
130
|
-
if (!this.aborted) {
|
|
131
|
-
try {
|
|
132
|
-
this.dispatch(this.opts, this)
|
|
133
|
-
} catch (err2) {
|
|
134
|
-
this.handler.onError(new AggregateError([err, err2]))
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
.catch((err) => {
|
|
139
|
-
if (!this.aborted) {
|
|
140
|
-
this.handler.onError(err)
|
|
141
|
-
}
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
this.error = err
|
|
145
|
-
this.opts = {
|
|
146
|
-
...this.opts,
|
|
147
|
-
headers: {
|
|
148
|
-
...this.opts.headers,
|
|
149
|
-
'if-match': this.etag,
|
|
150
|
-
range: `bytes=${this.pos}-${this.end ?? ''}`,
|
|
151
|
-
},
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
this.opts.logger?.debug('retrying response body')
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export default (dispatch) => (opts, handler) => {
|
|
159
|
-
return opts.idempotent && opts.retry && opts.method === 'GET' && !opts.upgrade
|
|
160
|
-
? dispatch(opts, new Handler(opts, { handler, dispatch }))
|
|
161
|
-
: dispatch(opts, handler)
|
|
162
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { parseHeaders, isDisturbed, retry as retryFn } from '../utils.js'
|
|
2
|
-
import createError from '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.retryCount = 0
|
|
14
|
-
this.retryPromise = 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
|
-
onUpgrade(statusCode, rawHeaders, socket) {
|
|
35
|
-
return this.handler.onUpgrade(statusCode, rawHeaders, socket)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
onBodySent(chunk) {
|
|
39
|
-
return this.handler.onBodySent(chunk)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
onRequestSent() {
|
|
43
|
-
return this.handler.onRequestSent()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
47
|
-
if (statusCode < 400) {
|
|
48
|
-
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const err = createError(statusCode, { headers: parseHeaders(rawHeaders) })
|
|
52
|
-
|
|
53
|
-
const retryPromise = retryFn(err, this.retryCount++, this.opts)
|
|
54
|
-
if (retryPromise == null) {
|
|
55
|
-
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
retryPromise.catch(() => {})
|
|
59
|
-
|
|
60
|
-
this.retryPromise = retryPromise
|
|
61
|
-
|
|
62
|
-
this.abort(err)
|
|
63
|
-
|
|
64
|
-
return false
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
onData(chunk) {
|
|
68
|
-
return this.handler.onData(chunk)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
onComplete(rawTrailers) {
|
|
72
|
-
return this.handler.onComplete(rawTrailers)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
onError(err) {
|
|
76
|
-
if (this.retryPromise == null || this.aborted || isDisturbed(this.opts.body)) {
|
|
77
|
-
return this.handler.onError(err)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
this.retryPromise
|
|
81
|
-
.then(() => {
|
|
82
|
-
if (!this.aborted) {
|
|
83
|
-
try {
|
|
84
|
-
this.dispatch(this.opts, this)
|
|
85
|
-
} catch (err2) {
|
|
86
|
-
this.handler.onError(new AggregateError([err, err2]))
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
.catch((err) => {
|
|
91
|
-
if (!this.aborted) {
|
|
92
|
-
this.handler.onError(err)
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
this.retryPromise = null
|
|
96
|
-
|
|
97
|
-
this.opts.logger?.debug('retrying response status')
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export default (dispatch) => (opts, handler) =>
|
|
102
|
-
opts.idempotent && opts.retry
|
|
103
|
-
? dispatch(opts, new Handler(opts, { handler, dispatch }))
|
|
104
|
-
: dispatch(opts, handler)
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
if (this.signal.aborted) {
|
|
10
|
-
abort(this.signal.reason)
|
|
11
|
-
} else {
|
|
12
|
-
this.abort = () => abort(this.signal.reason)
|
|
13
|
-
this.signal.addEventListener('abort', this.abort)
|
|
14
|
-
|
|
15
|
-
this.handler.onConnect(abort)
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
onUpgrade(statusCode, rawHeaders, socket) {
|
|
20
|
-
return this.handler.onUpgrade(statusCode, rawHeaders, socket)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
onBodySent(chunk) {
|
|
24
|
-
return this.handler.onBodySent(chunk)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
onRequestSent() {
|
|
28
|
-
return this.handler.onRequestSent()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
32
|
-
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
onData(chunk) {
|
|
36
|
-
return this.handler.onData(chunk)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
onComplete(rawTrailers) {
|
|
40
|
-
if (this.abort) {
|
|
41
|
-
this.signal.removeEventListener('abort', this.abort)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return this.handler.onComplete(rawTrailers)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
onError(err) {
|
|
48
|
-
if (this.abort) {
|
|
49
|
-
this.signal.removeEventListener('abort', this.abort)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return this.handler.onError(err)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export default (dispatch) => (opts, handler) =>
|
|
57
|
-
opts.signal ? dispatch(opts, new Handler(opts, { handler })) : dispatch(opts, handler)
|