@superhero/http-request 4.0.8 → 4.0.10
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/LICENCE +21 -0
- package/index.js +97 -40
- package/index.test.js +2 -2
- package/package.json +2 -2
package/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Erik Landvall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/index.js
CHANGED
|
@@ -83,16 +83,19 @@ export default class Request
|
|
|
83
83
|
|
|
84
84
|
const url = new URL(authority || this.config.base)
|
|
85
85
|
authority = url.protocol + '//' + url.host
|
|
86
|
-
options = Object.assign(
|
|
86
|
+
options = Object.assign(this.config, options)
|
|
87
87
|
|
|
88
|
+
this.config.base = authority
|
|
89
|
+
this.config.url = url.pathname + url.search
|
|
88
90
|
this.http2Session = http2.connect(authority, options, () =>
|
|
89
91
|
{
|
|
90
|
-
this.config.base = authority
|
|
91
|
-
this.config.url = url.pathname + url.search
|
|
92
92
|
this.http2Session.removeAllListeners('error')
|
|
93
|
+
this.http2Session.on('error', console.error)
|
|
94
|
+
|
|
93
95
|
accept()
|
|
94
96
|
})
|
|
95
97
|
|
|
98
|
+
// If there is a error on connection, reject the promise.
|
|
96
99
|
this.http2Session.once('error', (reason) =>
|
|
97
100
|
{
|
|
98
101
|
const error = new Error(`Failed to connect to server over HTTP2 using authority: ${authority}`)
|
|
@@ -100,12 +103,6 @@ export default class Request
|
|
|
100
103
|
error.cause = reason
|
|
101
104
|
reject(error)
|
|
102
105
|
})
|
|
103
|
-
|
|
104
|
-
this.http2Session.once('close', () =>
|
|
105
|
-
{
|
|
106
|
-
this.http2Session.removeAllListeners()
|
|
107
|
-
delete this.http2Session
|
|
108
|
-
})
|
|
109
106
|
})
|
|
110
107
|
}
|
|
111
108
|
|
|
@@ -118,23 +115,43 @@ export default class Request
|
|
|
118
115
|
{
|
|
119
116
|
return new Promise((accept, reject) =>
|
|
120
117
|
{
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
const http2Session = this.http2Session
|
|
119
|
+
|
|
120
|
+
if(http2Session)
|
|
123
121
|
{
|
|
124
|
-
|
|
122
|
+
http2Session.removeAllListeners()
|
|
123
|
+
|
|
124
|
+
if(false === http2Session.closed)
|
|
125
125
|
{
|
|
126
|
-
error
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
http2Session.close((error) =>
|
|
127
|
+
{
|
|
128
|
+
delete this.http2Session
|
|
129
|
+
|
|
130
|
+
error
|
|
131
|
+
? reject(error)
|
|
132
|
+
: accept()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
return // await the close event
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
delete this.http2Session
|
|
134
139
|
}
|
|
140
|
+
|
|
141
|
+
// fallback to accept if nothing to close
|
|
142
|
+
accept()
|
|
135
143
|
})
|
|
136
144
|
}
|
|
137
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Reonnects to a HTTP/2 server using the last used configurations.
|
|
148
|
+
*/
|
|
149
|
+
async reconnect()
|
|
150
|
+
{
|
|
151
|
+
await this.close()
|
|
152
|
+
await this.connect()
|
|
153
|
+
}
|
|
154
|
+
|
|
138
155
|
/**
|
|
139
156
|
* GET – Read a resource or a collection of resources.
|
|
140
157
|
* Used to retrieve data without modifying it.
|
|
@@ -245,7 +262,18 @@ export default class Request
|
|
|
245
262
|
*
|
|
246
263
|
* @param {string} method
|
|
247
264
|
* @param {RequestOptions} options
|
|
265
|
+
*
|
|
248
266
|
* @returns {RequestResponse}
|
|
267
|
+
*
|
|
268
|
+
* @throws {Error} E_HTTP_REQUEST_CLIENT_ERROR
|
|
269
|
+
* @throws {Error} E_HTTP_REQUEST_CLIENT_TIMEOUT
|
|
270
|
+
* @throws {Error} E_HTTP_REQUEST_DOWNSTREAM_ERROR
|
|
271
|
+
* @throws {Error} E_HTTP_REQUEST_INVALID_RESPONSE_BODY_FORMAT
|
|
272
|
+
* @throws {Error} E_HTTP_REQUEST_INVALID_RESPONSE_STATUS
|
|
273
|
+
* @throws {Error} E_HTTP_REQUEST_HTTP2_SESSION_DESTROYED
|
|
274
|
+
* @throws {Error} E_HTTP_REQUEST_HTTP2_SESSION_CLOSED
|
|
275
|
+
* @throws {Error} E_HTTP_REQUEST_RETRY_HTTP2_RECONNECT
|
|
276
|
+
* @throws {Error} E_HTTP_REQUEST_RETRY_ERROR
|
|
249
277
|
*/
|
|
250
278
|
#fetch(method, options)
|
|
251
279
|
{
|
|
@@ -284,11 +312,6 @@ export default class Request
|
|
|
284
312
|
* @param {RequestOptions} options
|
|
285
313
|
*
|
|
286
314
|
* @returns {RequestResponse}
|
|
287
|
-
*
|
|
288
|
-
* @throws {Error} E_HTTP_REQUEST_CLIENT_TIMEOUT
|
|
289
|
-
* @throws {Error} E_HTTP_REQUEST_CLIENT_ERROR
|
|
290
|
-
* @throws {Error} E_HTTP_REQUEST_DOWNSTREAM_ERROR
|
|
291
|
-
* @throws {Error} E_HTTP_REQUEST_INVALID_RESPONSE_BODY_FORMAT
|
|
292
315
|
*/
|
|
293
316
|
#resolve(options)
|
|
294
317
|
{
|
|
@@ -315,6 +338,22 @@ export default class Request
|
|
|
315
338
|
|
|
316
339
|
#resolveHttp2Client(options, method, headers, url, accept, reject)
|
|
317
340
|
{
|
|
341
|
+
if(true === this.http2Session.destroyed)
|
|
342
|
+
{
|
|
343
|
+
const error = new Error('Session destroyed')
|
|
344
|
+
error.code = 'E_HTTP_REQUEST_HTTP2_SESSION_DESTROYED'
|
|
345
|
+
error.cause = `Can not perform request over a destroyed HTTP2 session to: ${url}`
|
|
346
|
+
return reject(error)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if(true === this.http2Session.closed)
|
|
350
|
+
{
|
|
351
|
+
const error = new Error('Session closed')
|
|
352
|
+
error.code = 'E_HTTP_REQUEST_HTTP2_SESSION_CLOSED'
|
|
353
|
+
error.cause = `Can not perform request over a closed HTTP2 session to: ${url}`
|
|
354
|
+
return reject(error)
|
|
355
|
+
}
|
|
356
|
+
|
|
318
357
|
delete headers['transfer-encoding']
|
|
319
358
|
|
|
320
359
|
const { pathname, search } = new URL(url)
|
|
@@ -369,7 +408,7 @@ export default class Request
|
|
|
369
408
|
#connectionClosed(upstream, reject)
|
|
370
409
|
{
|
|
371
410
|
upstream.removeAllListeners()
|
|
372
|
-
const error = new Error('
|
|
411
|
+
const error = new Error('Connection was closed unexpectedly')
|
|
373
412
|
error.code = 'E_HTTP_REQUEST_CLIENT_ERROR'
|
|
374
413
|
setImmediate(() => reject(error)) // this error is a fallback if the promise is not already resolved
|
|
375
414
|
}
|
|
@@ -419,12 +458,8 @@ export default class Request
|
|
|
419
458
|
|
|
420
459
|
/**
|
|
421
460
|
* Resolves the request in a retry loop.
|
|
422
|
-
*
|
|
423
461
|
* @param {RequestOptions} options
|
|
424
|
-
*
|
|
425
462
|
* @returns {RequestResponse}
|
|
426
|
-
*
|
|
427
|
-
* @throws {Error} E_HTTP_REQUEST_RETRY
|
|
428
463
|
*/
|
|
429
464
|
async #resolveRetryLoop(options)
|
|
430
465
|
{
|
|
@@ -458,11 +493,23 @@ export default class Request
|
|
|
458
493
|
}
|
|
459
494
|
else
|
|
460
495
|
{
|
|
461
|
-
this.#resolveRetryLoopError(options, errorTrace, error)
|
|
496
|
+
await this.#resolveRetryLoopError(options, errorTrace, error)
|
|
462
497
|
await wait(options.retryDelay)
|
|
463
498
|
}
|
|
464
499
|
}
|
|
465
500
|
}
|
|
501
|
+
|
|
502
|
+
if(errorTrace.every((error) => error.code === errorTrace[0].code))
|
|
503
|
+
{
|
|
504
|
+
throw errorTrace.pop()
|
|
505
|
+
}
|
|
506
|
+
else
|
|
507
|
+
{
|
|
508
|
+
const error = new Error('Multiple types of errors occurred during the retry loop')
|
|
509
|
+
error.code = 'E_HTTP_REQUEST_RETRY_ERROR'
|
|
510
|
+
error.cause = errorTrace
|
|
511
|
+
throw error
|
|
512
|
+
}
|
|
466
513
|
}
|
|
467
514
|
|
|
468
515
|
/**
|
|
@@ -473,14 +520,8 @@ export default class Request
|
|
|
473
520
|
* @param {Error} error
|
|
474
521
|
*
|
|
475
522
|
* @returns {Void}
|
|
476
|
-
*
|
|
477
|
-
* @throws {Error} E_HTTP_REQUEST_CLIENT_ERROR
|
|
478
|
-
* @throws {Error} E_HTTP_REQUEST_CLIENT_TIMEOUT
|
|
479
|
-
* @throws {Error} E_HTTP_REQUEST_DOWNSTREAM_ERROR
|
|
480
|
-
* @throws {Error} E_HTTP_REQUEST_INVALID_RESPONSE_BODY_FORMAT
|
|
481
|
-
* @throws {Error} E_HTTP_REQUEST_INVALID_RESPONSE_STATUS
|
|
482
523
|
*/
|
|
483
|
-
#resolveRetryLoopError(options, errorTrace, error)
|
|
524
|
+
async #resolveRetryLoopError(options, errorTrace, error)
|
|
484
525
|
{
|
|
485
526
|
switch(error.code)
|
|
486
527
|
{
|
|
@@ -488,6 +529,22 @@ export default class Request
|
|
|
488
529
|
{
|
|
489
530
|
return errorTrace.push(error)
|
|
490
531
|
}
|
|
532
|
+
case 'E_HTTP_REQUEST_HTTP2_SESSION_DESTROYED':
|
|
533
|
+
case 'E_HTTP_REQUEST_HTTP2_SESSION_CLOSED':
|
|
534
|
+
{
|
|
535
|
+
try
|
|
536
|
+
{
|
|
537
|
+
await this.reconnect()
|
|
538
|
+
}
|
|
539
|
+
catch(reason)
|
|
540
|
+
{
|
|
541
|
+
const reconnectError = new Error(`${reason.message}, retry to reconnect to the server failed`)
|
|
542
|
+
reconnectError.code = 'E_HTTP_REQUEST_RETRY_HTTP2_RECONNECT'
|
|
543
|
+
reconnectError.cause = reason
|
|
544
|
+
throw reason
|
|
545
|
+
}
|
|
546
|
+
return errorTrace.push(error)
|
|
547
|
+
}
|
|
491
548
|
case 'E_HTTP_REQUEST_CLIENT_TIMEOUT':
|
|
492
549
|
{
|
|
493
550
|
if(options.retryOnClientTimeout)
|
|
@@ -546,8 +603,8 @@ export default class Request
|
|
|
546
603
|
#createBodyHeaderDelimiter(body, isStreamed)
|
|
547
604
|
{
|
|
548
605
|
return isStreamed
|
|
549
|
-
? { 'transfer-encoding': 'chunked' }
|
|
550
|
-
: { 'content-length'
|
|
606
|
+
? { 'transfer-encoding' : 'chunked' }
|
|
607
|
+
: { 'content-length' : body?.length || 0 }
|
|
551
608
|
}
|
|
552
609
|
|
|
553
610
|
/**
|
package/index.test.js
CHANGED
|
@@ -376,14 +376,14 @@ suite('@superhero/http-request', () =>
|
|
|
376
376
|
const options =
|
|
377
377
|
{
|
|
378
378
|
url : '/retry-on-status',
|
|
379
|
-
retry : 2,
|
|
379
|
+
retry : 2,
|
|
380
380
|
retryDelay : 100,
|
|
381
381
|
retryOnStatus : [ 503 ]
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
await assert.rejects(
|
|
385
385
|
request.get(options),
|
|
386
|
-
|
|
386
|
+
{ code:'E_HTTP_REQUEST_INVALID_RESPONSE_STATUS' },
|
|
387
387
|
'Should throw an error after the second attempt')
|
|
388
388
|
|
|
389
389
|
assert.equal(attempt, 2, 'Should make exactly 2 attempts')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superhero/http-request",
|
|
3
|
-
"version": "4.0.
|
|
4
|
-
"description": "HTTP request component supporting HTTP 1.1 and HTTP 2.0",
|
|
3
|
+
"version": "4.0.10",
|
|
4
|
+
"description": "HTTP(S) request component supporting HTTP 1.1 and HTTP 2.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"http request",
|
|
7
7
|
"http client",
|