@superhero/http-request 4.1.0 → 4.1.1

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.
Files changed (3) hide show
  1. package/index.js +60 -62
  2. package/index.test.js +45 -1
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -68,8 +68,8 @@ export default class Request
68
68
  /**
69
69
  * Connects to a HTTP/2 server.
70
70
  *
71
- * @param {String} authority the URL
72
- * @param {Object} [options]
71
+ * @param {String|Object} [authority] the URL to connect to the server, or the options object
72
+ * @param {Object} [options] @see node:http2.connect options
73
73
  *
74
74
  * @throws {Error} E_HTTP_REQUEST_CONNECT_INVALID_ARGUMENT
75
75
  *
@@ -77,31 +77,28 @@ export default class Request
77
77
  */
78
78
  connect(authority, options)
79
79
  {
80
- return new Promise(async (accept, reject) =>
80
+ return new Promise(async resolve =>
81
81
  {
82
82
  await this.close()
83
83
 
84
- const url = new URL(authority || this.config.base)
85
- authority = url.protocol + '//' + url.host
86
- options = Object.assign(this.config, options)
84
+ if('object' === typeof authority
85
+ && null !== authority
86
+ && false === !!options)
87
+ {
88
+ options = authority
89
+ authority = null
90
+ }
91
+
92
+ options = Object.assign(this.config, options)
93
+ const url = new URL(authority || options.authority || options.base || options.url)
87
94
 
88
- this.config.base = authority
89
- this.config.url = url.pathname + url.search
90
- this.http2Session = http2.connect(authority, options, () =>
95
+ this.config.authority = url.origin
96
+ this.http2Session = http2.connect(url.origin, options, () =>
91
97
  {
92
98
  this.http2Session.removeAllListeners('error')
93
99
  this.http2Session.on('error', console.error)
94
100
 
95
- accept()
96
- })
97
-
98
- // If there is a error on connection, reject the promise.
99
- this.http2Session.once('error', (reason) =>
100
- {
101
- const error = new Error(`Failed to connect to server over HTTP2 using authority: ${authority}`)
102
- error.code = 'E_HTTP_REQUEST_CONNECT_ERROR'
103
- error.cause = reason
104
- reject(error)
101
+ resolve()
105
102
  })
106
103
  })
107
104
  }
@@ -113,7 +110,7 @@ export default class Request
113
110
  */
114
111
  close()
115
112
  {
116
- return new Promise((accept, reject) =>
113
+ return new Promise((resolve, reject) =>
117
114
  {
118
115
  const http2Session = this.http2Session
119
116
 
@@ -129,7 +126,7 @@ export default class Request
129
126
 
130
127
  error
131
128
  ? reject(error)
132
- : accept()
129
+ : resolve()
133
130
  })
134
131
 
135
132
  return // await the close event
@@ -138,8 +135,8 @@ export default class Request
138
135
  delete this.http2Session
139
136
  }
140
137
 
141
- // fallback to accept if nothing to close
142
- accept()
138
+ // fallback to resolve if nothing to close
139
+ resolve()
143
140
  })
144
141
  }
145
142
 
@@ -282,13 +279,6 @@ export default class Request
282
279
  options = { url:options }
283
280
  }
284
281
 
285
- if(this.config.url && options.url)
286
- {
287
- options.url = options.url[0] === '/'
288
- ? options.url
289
- : this.config.url + options.url
290
- }
291
-
292
282
  options = Object.assign(
293
283
  {
294
284
  method,
@@ -325,20 +315,20 @@ export default class Request
325
315
  */
326
316
  #resolve(options)
327
317
  {
328
- return new Promise((accept, reject) =>
318
+ return new Promise((resolve, reject) =>
329
319
  {
330
320
  const
331
321
  method = options.method,
332
322
  headers = this.#normalizeHeaders(options.headers),
333
323
  body = this.#normalizeBody(options.body ?? options.data, headers['content-type']),
334
324
  delimiter = this.#createBodyHeaderDelimiter(body, !!options.upstream || !!this.http2Session),
335
- url = this.#normalizeUrl(options.url, options.base)
325
+ url = this.#normalizeUrl(options.authority, options.base, options.url)
336
326
 
337
327
  Object.assign(headers, delimiter)
338
328
 
339
329
  const upstream = this.http2Session
340
- ? this.#resolveHttp2Client(options, method, headers, url, accept, reject)
341
- : this.#resolveHttp1Client(options, method, headers, url, accept, reject)
330
+ ? this.#resolveHttp2Client(options, method, headers, url, resolve, reject)
331
+ : this.#resolveHttp1Client(options, method, headers, url, resolve, reject)
342
332
 
343
333
  options.upstream
344
334
  ? options.upstream.pipe(upstream)
@@ -346,7 +336,7 @@ export default class Request
346
336
  })
347
337
  }
348
338
 
349
- #resolveHttp2Client(options, method, headers, url, accept, reject)
339
+ #resolveHttp2Client(options, method, headers, url, resolve, reject)
350
340
  {
351
341
  if(true === this.http2Session.destroyed)
352
342
  {
@@ -394,13 +384,13 @@ export default class Request
394
384
  delete upstream.headers[HEADER_STATUS]
395
385
  Object.defineProperty(upstream.headers, SENSITIVE_HEADERS, { enumerable:false, value:headers[SENSITIVE_HEADERS] })
396
386
 
397
- this.#resolveOnResponse(options, method, url, accept, reject, upstream)
387
+ this.#resolveOnResponse(options, method, url, resolve, reject, upstream)
398
388
  })
399
389
 
400
390
  return upstream
401
391
  }
402
392
 
403
- #resolveHttp1Client(options, method, headers, url, accept, reject)
393
+ #resolveHttp1Client(options, method, headers, url, resolve, reject)
404
394
  {
405
395
  const
406
396
  request = url.startsWith('https:') ? https.request : http.request,
@@ -410,7 +400,7 @@ export default class Request
410
400
  upstream.on('close', this.#connectionClosed .bind(this, upstream, reject))
411
401
  upstream.on('error', this.#resolveOnClientError .bind(this, method, url, reject))
412
402
  upstream.on('timeout', this.#resolveOnClientTimeout.bind(this, upstream, options.timeout, method, url, reject))
413
- upstream.on('response', this.#resolveOnResponse .bind(this, options, method, url, accept, reject))
403
+ upstream.on('response', this.#resolveOnResponse .bind(this, options, method, url, resolve, reject))
414
404
 
415
405
  return upstream
416
406
  }
@@ -429,13 +419,13 @@ export default class Request
429
419
  * @param {RequestOptions} options
430
420
  * @param {String} method
431
421
  * @param {String} url
432
- * @param {Function} accept Promise accept
422
+ * @param {Function} resolve Promise resolve
433
423
  * @param {Function} reject Promise reject
434
424
  * @param {http.IncomingMessage} readable
435
425
  *
436
426
  * @returns {Void}
437
427
  */
438
- #resolveOnResponse(options, method, url, accept, reject, readable)
428
+ #resolveOnResponse(options, method, url, resolve, reject, readable)
439
429
  {
440
430
  const response =
441
431
  {
@@ -458,11 +448,11 @@ export default class Request
458
448
  {
459
449
  readable.pipe(options.downstream)
460
450
  readable.resume()
461
- accept(response)
451
+ resolve(response)
462
452
  }
463
453
  else
464
454
  {
465
- this.#bufferResponseBody(readable, response, method, url, accept, reject)
455
+ this.#bufferResponseBody(readable, response, method, url, resolve, reject)
466
456
  }
467
457
  }
468
458
 
@@ -509,19 +499,16 @@ export default class Request
509
499
  }
510
500
  }
511
501
 
512
- const
513
- uniqueReasons = reasons.filter((reason, i) => [i, -1].includes(reasons.lastIndexOf(({ code }) => code === reason.code))),
514
- reason = uniqueReasons.pop()
502
+ const
503
+ unique = reasons.filter((reason, i) => [i, -1].includes(reasons.map(e => e.code).lastIndexOf(code => code === reason.code))),
504
+ reason = unique.pop()
515
505
 
516
- if(uniqueReasons.length === 1)
506
+ if(unique.length)
517
507
  {
518
- throw reason
519
- }
520
- else
521
- {
522
- reason.previous = uniqueReasons
523
- throw reason
508
+ reason.retried = unique
524
509
  }
510
+
511
+ throw reason
525
512
  }
526
513
 
527
514
  /**
@@ -662,17 +649,28 @@ export default class Request
662
649
 
663
650
  /**
664
651
  * Normalizes the URL.
652
+ * One of authority or base must be provided,
665
653
  *
666
- * @param {String} url
667
- * @param {String} base
654
+ * @param {String} [base]
655
+ * @param {String} [authority] the URL origin (e.g. https://example.com)
656
+ * @param {String} [url]
668
657
  *
669
658
  * @returns {String} Normalized URL
670
659
  */
671
- #normalizeUrl(url, base)
660
+ #normalizeUrl(authority, base, url)
672
661
  {
662
+ let baseURL = authority && base
663
+ ? new URL(base, authority)
664
+ : new URL(base || authority)
665
+
666
+ if(url && false === baseURL.pathname.endsWith('/'))
667
+ {
668
+ baseURL = new URL(baseURL.pathname + '/', baseURL.href)
669
+ }
670
+
673
671
  return url
674
- ? new URL(url, base).toString()
675
- : new URL(base).toString()
672
+ ? new URL(url, baseURL.href).href
673
+ : baseURL.href
676
674
  }
677
675
 
678
676
  /**
@@ -723,12 +721,12 @@ export default class Request
723
721
  upstream.destroy(error)
724
722
  }
725
723
 
726
- #bufferResponseBody(readable, response, method, url, accept, reject)
724
+ #bufferResponseBody(readable, response, method, url, resolve, reject)
727
725
  {
728
726
  response.body = ''
729
727
  readable.on('data', (chunk) => response.body += chunk)
730
728
  readable.on('error', this.#onStreamError.bind(this, method, url, response))
731
- readable.on('end', this.#onStreamEnd .bind(this, method, url, response, accept, reject))
729
+ readable.on('end', this.#onStreamEnd .bind(this, method, url, response, resolve, reject))
732
730
  readable.resume()
733
731
  }
734
732
 
@@ -759,14 +757,14 @@ export default class Request
759
757
  * @param {String} method
760
758
  * @param {String} url
761
759
  * @param {http.IncomingMessage} response
762
- * @param {Function} accept promise accept
760
+ * @param {Function} resolve promise resolve
763
761
  * @param {Function} reject promise reject
764
762
  *
765
763
  * @returns {void}
766
764
  *
767
765
  * @throws {Error} E_HTTP_REQUEST_INVALID_RESPONSE_BODY_FORMAT
768
766
  */
769
- #onStreamEnd(method, url, response, accept, reject)
767
+ #onStreamEnd(method, url, response, resolve, reject)
770
768
  {
771
769
  try
772
770
  {
@@ -791,7 +789,7 @@ export default class Request
791
789
  return
792
790
  }
793
791
 
794
- accept(response)
792
+ resolve(response)
795
793
  }
796
794
 
797
795
  #contentTypeApplicationJson(body)
package/index.test.js CHANGED
@@ -53,6 +53,50 @@ suite('@superhero/http-request', () =>
53
53
  assert.equal(response.body.url, '/', 'Should result to /')
54
54
  })
55
55
 
56
+ test('Request using a relative path', async sub =>
57
+ {
58
+ // ...close the server that was started before the
59
+ // sub tests starts another server for each test...
60
+ await request.close()
61
+ await new Promise(resolve => server.close(resolve))
62
+
63
+ await sub.test('Request using an absolute path with a trailing base path slash', async () =>
64
+ {
65
+ const base = request.config.base || ''
66
+ request.config.base = base + '/path/'
67
+ const response = await request.get('/foobar')
68
+ assert.equal(response.status, 200, 'Should return a 200 status code')
69
+ assert.equal(response.body.url, '/foobar', 'Should result to the absolute path')
70
+ })
71
+
72
+ await sub.test('Request using a absolute path with out a trailing base path slash', async () =>
73
+ {
74
+ const base = request.config.base || ''
75
+ request.config.base = base + '/path'
76
+ const response = await request.get('/foobar')
77
+ assert.equal(response.status, 200, 'Should return a 200 status code')
78
+ assert.equal(response.body.url, '/foobar', 'Should result to the absolute path')
79
+ })
80
+
81
+ await sub.test('Request using a relative path with a trailing base path slash', async () =>
82
+ {
83
+ const base = request.config.base || ''
84
+ request.config.base = base + '/path/'
85
+ const response = await request.get('foobar')
86
+ assert.equal(response.status, 200, 'Should return a 200 status code')
87
+ assert.equal(response.body.url, '/path/foobar', 'Should result to the absolute path')
88
+ })
89
+
90
+ await sub.test('Request using a relative path with out a trailing base path slash', async () =>
91
+ {
92
+ const base = request.config.base || ''
93
+ request.config.base = base + '/path'
94
+ const response = await request.get('foobar')
95
+ assert.equal(response.status, 200, 'Should return a 200 status code')
96
+ assert.equal(response.body.url, '/path/foobar', 'Should result to the absolute path')
97
+ })
98
+ })
99
+
56
100
  test('Request using a string body', async () =>
57
101
  {
58
102
  const response = await request.post({ body: 'test body' })
@@ -503,7 +547,7 @@ suite('@superhero/http-request', () =>
503
547
  })
504
548
  })
505
549
 
506
- afterEach((done) => request.close().then(() => server.close(done)))
550
+ afterEach(done => request.close().then(() => server.close(done)))
507
551
 
508
552
  executeTheSameTestSuitFor_http1_http2()
509
553
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superhero/http-request",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "HTTP(S) request component supporting HTTP 1.1 and HTTP 2.0",
5
5
  "keywords": [
6
6
  "http request",