undici 8.0.3 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/docs/docs/api/Client.md +2 -0
  2. package/docs/docs/api/Dispatcher.md +2 -2
  3. package/lib/api/api-connect.js +1 -1
  4. package/lib/api/api-pipeline.js +2 -2
  5. package/lib/api/api-request.js +2 -2
  6. package/lib/api/api-stream.js +1 -1
  7. package/lib/api/api-upgrade.js +8 -2
  8. package/lib/api/readable.js +3 -2
  9. package/lib/cache/memory-cache-store.js +1 -1
  10. package/lib/cache/sqlite-cache-store.js +6 -4
  11. package/lib/core/connect.js +16 -0
  12. package/lib/core/constants.js +1 -24
  13. package/lib/core/errors.js +2 -2
  14. package/lib/core/request.js +17 -2
  15. package/lib/core/socks5-client.js +24 -9
  16. package/lib/core/socks5-utils.js +32 -23
  17. package/lib/core/util.js +28 -3
  18. package/lib/dispatcher/agent.js +38 -40
  19. package/lib/dispatcher/balanced-pool.js +21 -23
  20. package/lib/dispatcher/client-h1.js +34 -16
  21. package/lib/dispatcher/client-h2.js +400 -147
  22. package/lib/dispatcher/client.js +3 -2
  23. package/lib/dispatcher/dispatcher-base.js +18 -0
  24. package/lib/dispatcher/h2c-client.js +4 -4
  25. package/lib/dispatcher/pool-base.js +6 -6
  26. package/lib/dispatcher/pool.js +8 -3
  27. package/lib/dispatcher/proxy-agent.js +2 -0
  28. package/lib/dispatcher/round-robin-pool.js +5 -6
  29. package/lib/dispatcher/socks5-proxy-agent.js +23 -14
  30. package/lib/handler/cache-handler.js +1 -1
  31. package/lib/handler/redirect-handler.js +4 -0
  32. package/lib/interceptor/redirect.js +3 -3
  33. package/lib/llhttp/llhttp-wasm.js +1 -1
  34. package/lib/llhttp/llhttp_simd-wasm.js +1 -1
  35. package/lib/mock/mock-agent.js +8 -8
  36. package/lib/mock/mock-call-history.js +15 -15
  37. package/lib/util/cache.js +1 -1
  38. package/lib/web/eventsource/eventsource-stream.js +245 -150
  39. package/lib/web/fetch/formdata-parser.js +17 -6
  40. package/lib/web/fetch/index.js +38 -28
  41. package/lib/web/webidl/index.js +5 -5
  42. package/lib/web/websocket/frame.js +1 -7
  43. package/lib/web/websocket/permessage-deflate.js +13 -31
  44. package/lib/web/websocket/receiver.js +62 -22
  45. package/lib/web/websocket/stream/websocketstream.js +6 -5
  46. package/lib/web/websocket/websocket.js +6 -1
  47. package/package.json +1 -1
  48. package/types/client.d.ts +11 -0
  49. package/types/dispatcher.d.ts +4 -4
  50. package/types/header.d.ts +5 -0
  51. package/types/interceptors.d.ts +1 -1
  52. package/types/proxy-agent.d.ts +2 -2
  53. package/types/socks5-proxy-agent.d.ts +2 -2
  54. package/lib/llhttp/.gitkeep +0 -0
@@ -41,6 +41,12 @@ const {
41
41
  const { channels } = require('../core/diagnostics.js')
42
42
 
43
43
  const kOpenStreams = Symbol('open streams')
44
+ const kRequestStreamId = Symbol('request stream id')
45
+ const kRequestStream = Symbol('request stream')
46
+ const kRequestStreamCleanup = Symbol('request stream cleanup')
47
+ const kRequestStreamOnData = Symbol('request stream on data')
48
+ const kRequestStreamOnCloseError = Symbol('request stream on close error')
49
+ const kReceivedGoAway = Symbol('received goaway')
44
50
 
45
51
  let extractBody
46
52
 
@@ -63,29 +69,73 @@ const {
63
69
  HTTP2_HEADER_EXPECT,
64
70
  HTTP2_HEADER_STATUS,
65
71
  HTTP2_HEADER_PROTOCOL,
66
- NGHTTP2_REFUSED_STREAM,
67
- NGHTTP2_CANCEL
72
+ NGHTTP2_NO_ERROR,
73
+ NGHTTP2_REFUSED_STREAM
68
74
  }
69
75
  } = http2
70
76
 
71
- function parseH2Headers (headers) {
72
- const result = []
73
-
74
- for (const [name, value] of Object.entries(headers)) {
75
- // h2 may concat the header value by array
76
- // e.g. Set-Cookie
77
- if (Array.isArray(value)) {
78
- for (const subvalue of value) {
79
- // we need to provide each header value of header name
80
- // because the headers handler expect name-value pair
81
- result.push(Buffer.from(name), Buffer.from(subvalue))
82
- }
83
- } else {
84
- result.push(Buffer.from(name), Buffer.from(value))
77
+ function getGoAwayError (session, errorCode) {
78
+ return session[kError] ||
79
+ (errorCode === NGHTTP2_NO_ERROR
80
+ ? new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`)
81
+ : new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(session[kSocket])))
82
+ }
83
+
84
+ function getGoAwayPendingIdx (client, lastStreamID) {
85
+ const maxAcceptedStreamID = Number.isInteger(lastStreamID) ? lastStreamID : Number.MAX_SAFE_INTEGER
86
+
87
+ for (let i = client[kRunningIdx]; i < client[kPendingIdx]; i++) {
88
+ const request = client[kQueue][i]
89
+
90
+ if (request == null) {
91
+ continue
92
+ }
93
+
94
+ if (typeof request[kRequestStreamId] !== 'number' || request[kRequestStreamId] > maxAcceptedStreamID) {
95
+ return i
85
96
  }
86
97
  }
87
98
 
88
- return result
99
+ return client[kPendingIdx]
100
+ }
101
+
102
+ function detachRequestFromStream (request) {
103
+ request[kRequestStreamId] = null
104
+ request[kRequestStream] = null
105
+ request[kRequestStreamCleanup] = null
106
+ }
107
+
108
+ function bindRequestToStream (request, stream, cleanup) {
109
+ const previousCleanup = request[kRequestStreamCleanup]
110
+ detachRequestFromStream(request)
111
+ previousCleanup?.()
112
+ request[kRequestStreamId] = stream.id
113
+ request[kRequestStream] = stream
114
+ request[kRequestStreamCleanup] = cleanup
115
+ }
116
+
117
+ function clearRequestStream (request) {
118
+ const cleanup = request[kRequestStreamCleanup]
119
+ detachRequestFromStream(request)
120
+ cleanup?.()
121
+ }
122
+
123
+ function canRetryRequestAfterGoAway (request) {
124
+ const { body } = request
125
+
126
+ return body == null || util.isBuffer(body) || util.isBlobLike(body)
127
+ }
128
+
129
+ function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) {
130
+ const stream = request[kRequestStream]
131
+
132
+ clearRequestStream(request)
133
+
134
+ if (stream != null && !stream.destroyed && !stream.closed) {
135
+ try {
136
+ stream.close(code)
137
+ } catch {}
138
+ }
89
139
  }
90
140
 
91
141
  function connectH2 (client, socket) {
@@ -113,6 +163,7 @@ function connectH2 (client, socket) {
113
163
  interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
114
164
  }
115
165
  }
166
+ session[kReceivedGoAway] = false
116
167
  // We set it to true by default in a best-effort; however once connected to an H2 server
117
168
  // we will check if extended CONNECT protocol is supported or not
118
169
  // and set this value accordingly.
@@ -184,6 +235,14 @@ function connectH2 (client, socket) {
184
235
  * @returns {boolean}
185
236
  */
186
237
  busy (request) {
238
+ if (session[kRemoteSettings] === false && client[kRunning] > 0) {
239
+ return true
240
+ }
241
+
242
+ if (client[kRunning] >= client[kMaxConcurrentStreams]) {
243
+ return true
244
+ }
245
+
187
246
  if (request != null) {
188
247
  if (client[kRunning] > 0) {
189
248
  // We are already processing requests
@@ -273,7 +332,7 @@ function onHttp2SendPing (session) {
273
332
 
274
333
  function onPing (err, duration) {
275
334
  const client = this[kClient]
276
- const socket = this[kClient]
335
+ const socket = this[kSocket]
277
336
 
278
337
  if (err != null) {
279
338
  const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`)
@@ -313,48 +372,68 @@ function onHttp2SessionEnd () {
313
372
  *
314
373
  * @this {import('http2').ClientHttp2Session}
315
374
  * @param {number} errorCode
375
+ * @param {number} lastStreamID
316
376
  */
317
- function onHttp2SessionGoAway (errorCode) {
318
- // TODO(mcollina): Verify if GOAWAY implements the spec correctly:
319
- // https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
320
- // Specifically, we do not verify the "valid" stream id.
377
+ function onHttp2SessionGoAway (errorCode, lastStreamID) {
378
+ if (this[kReceivedGoAway]) {
379
+ return
380
+ }
381
+
382
+ this[kReceivedGoAway] = true
321
383
 
322
- const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(this[kSocket]))
384
+ const err = getGoAwayError(this, errorCode)
323
385
  const client = this[kClient]
386
+ const previousPendingIdx = client[kPendingIdx]
387
+ const pendingIdx = getGoAwayPendingIdx(client, lastStreamID)
388
+ const retriableRequests = []
324
389
 
325
- client[kSocket] = null
326
- client[kHTTPContext] = null
390
+ for (let i = pendingIdx; i < previousPendingIdx; i++) {
391
+ const request = client[kQueue][i]
327
392
 
328
- // this is an HTTP2 session
329
- this.close()
330
- this[kHTTP2Session] = null
393
+ if (request != null) {
394
+ closeRequestStream(request)
331
395
 
332
- util.destroy(this[kSocket], err)
396
+ if (canRetryRequestAfterGoAway(request)) {
397
+ retriableRequests.push(request)
398
+ } else {
399
+ util.errorRequest(client, request, err)
400
+ }
401
+ }
402
+ }
333
403
 
334
- // Fail head of pipeline.
335
- if (client[kRunningIdx] < client[kQueue].length) {
336
- const request = client[kQueue][client[kRunningIdx]]
337
- client[kQueue][client[kRunningIdx]++] = null
338
- util.errorRequest(client, request, err)
339
- client[kPendingIdx] = client[kRunningIdx]
404
+ if (pendingIdx !== previousPendingIdx) {
405
+ const remainingPendingRequests = client[kQueue].slice(previousPendingIdx)
406
+ client[kQueue].length = pendingIdx
407
+ client[kQueue].push(...retriableRequests, ...remainingPendingRequests)
340
408
  }
341
409
 
342
- assert(client[kRunning] === 0)
410
+ if (client[kHTTP2Session] === this) {
411
+ client[kSocket] = null
412
+ client[kHTTPContext] = null
413
+ client[kHTTP2Session] = null
414
+ }
415
+
416
+ if (!this.closed && !this.destroyed) {
417
+ this.close()
418
+ }
419
+
420
+ client[kPendingIdx] = pendingIdx
343
421
 
344
422
  client.emit('disconnect', client[kUrl], [client], err)
345
- client.emit('connectionError', client[kUrl], [client], err)
346
423
 
347
424
  client[kResume]()
348
425
  }
349
426
 
350
427
  function onHttp2SessionClose () {
351
- const { [kClient]: client, [kHTTP2SessionState]: state } = this
352
- const { [kSocket]: socket } = client
428
+ const { [kClient]: client, [kHTTP2SessionState]: state, [kSocket]: socket } = this
353
429
 
354
- const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
430
+ const err = socket[kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
355
431
 
356
- client[kSocket] = null
357
- client[kHTTPContext] = null
432
+ if (client[kHTTP2Session] === this) {
433
+ client[kSocket] = null
434
+ client[kHTTPContext] = null
435
+ client[kHTTP2Session] = null
436
+ }
358
437
 
359
438
  if (state.ping.interval != null) {
360
439
  clearInterval(state.ping.interval)
@@ -376,15 +455,27 @@ function onHttp2SessionClose () {
376
455
  function onHttp2SocketClose () {
377
456
  const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
378
457
 
379
- const client = this[kHTTP2Session][kClient]
458
+ const session = this[kHTTP2Session]
459
+ const client = session[kClient]
460
+
461
+ if (client[kSocket] !== this) {
462
+ // Ignore stale socket closes from a detached GOAWAY session and from any
463
+ // session that has already been replaced. If the session was detached
464
+ // without a GOAWAY and there is no replacement yet, we still need the
465
+ // close event to flush the client state.
466
+ if (session[kReceivedGoAway] || (client[kHTTP2Session] != null && client[kHTTP2Session] !== session)) {
467
+ return
468
+ }
469
+ }
380
470
 
381
471
  client[kSocket] = null
382
472
  client[kHTTPContext] = null
383
-
384
- if (this[kHTTP2Session] !== null) {
385
- this[kHTTP2Session].destroy(err)
473
+ if (client[kHTTP2Session] === session) {
474
+ client[kHTTP2Session] = null
386
475
  }
387
476
 
477
+ session.destroy(err)
478
+
388
479
  client[kPendingIdx] = client[kRunningIdx]
389
480
 
390
481
  assert(client[kRunning] === 0)
@@ -410,6 +501,37 @@ function onSocketClose () {
410
501
  this[kClosed] = true
411
502
  }
412
503
 
504
+ function noop () {}
505
+
506
+ function closeStreamSession (stream) {
507
+ const session = stream[kHTTP2Session]
508
+
509
+ stream[kHTTP2Session] = null
510
+ session[kOpenStreams] -= 1
511
+ if (session[kOpenStreams] === 0) {
512
+ session.unref()
513
+ }
514
+ }
515
+
516
+ function onUpgradeStreamClose () {
517
+ this.off('error', noop)
518
+
519
+ const failUpgradeStream = this[kRequestStreamOnCloseError]
520
+ this[kRequestStreamOnCloseError] = null
521
+
522
+ failUpgradeStream(new InformationalError('HTTP/2: stream closed before response headers'))
523
+ closeStreamSession(this)
524
+ }
525
+
526
+ function onRequestStreamClose () {
527
+ const onData = this[kRequestStreamOnData]
528
+
529
+ this[kRequestStreamOnData] = null
530
+ this.off('data', onData)
531
+ this.off('error', noop)
532
+ closeStreamSession(this)
533
+ }
534
+
413
535
  // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
414
536
  function shouldSendContentLength (method) {
415
537
  return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
@@ -464,7 +586,23 @@ function writeH2 (client, request) {
464
586
  headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
465
587
  headers[HTTP2_HEADER_METHOD] = method
466
588
 
467
- const abort = (err) => {
589
+ let requestFinalized = false
590
+ const finalizeRequest = (resetPendingIdx = false) => {
591
+ if (requestFinalized) {
592
+ return
593
+ }
594
+
595
+ requestFinalized = true
596
+ client[kQueue][client[kRunningIdx]++] = null
597
+
598
+ if (resetPendingIdx) {
599
+ client[kPendingIdx] = client[kRunningIdx]
600
+ }
601
+
602
+ client[kResume]()
603
+ }
604
+
605
+ const abort = (err, resetPendingIdx = false) => {
468
606
  if (request.aborted || request.completed) {
469
607
  return
470
608
  }
@@ -474,16 +612,14 @@ function writeH2 (client, request) {
474
612
  util.errorRequest(client, request, err)
475
613
 
476
614
  if (stream != null) {
477
- // Some chunks might still come after abort,
478
- // let's ignore them
479
- stream.removeAllListeners('data')
615
+ clearRequestStream(request)
480
616
 
481
617
  // On Abort, we close the stream to send RST_STREAM frame
482
618
  stream.close()
483
619
 
484
620
  // We move the running index to the next request
485
621
  client[kOnError](err)
486
- client[kResume]()
622
+ finalizeRequest(resetPendingIdx)
487
623
  }
488
624
 
489
625
  // We do not destroy the socket as we can continue using the session
@@ -491,6 +627,26 @@ function writeH2 (client, request) {
491
627
  util.destroy(body, err)
492
628
  }
493
629
 
630
+ const requestStream = (headers, options) => {
631
+ try {
632
+ return session.request(headers, options)
633
+ } catch (err) {
634
+ if (err?.code !== 'ERR_HTTP2_INVALID_CONNECTION_HEADERS') {
635
+ throw err
636
+ }
637
+
638
+ const wrappedErr = new InformationalError(err.message, { cause: err })
639
+ session[kError] = wrappedErr
640
+ session[kSocket][kError] = wrappedErr
641
+
642
+ session.destroy(wrappedErr)
643
+ util.destroy(session[kSocket], wrappedErr)
644
+ abort(wrappedErr)
645
+
646
+ return null
647
+ }
648
+ }
649
+
494
650
  try {
495
651
  // We are already connected, streams are pending.
496
652
  // We can call on connect, and wait for abort
@@ -506,6 +662,84 @@ function writeH2 (client, request) {
506
662
  if (upgrade || method === 'CONNECT') {
507
663
  session.ref()
508
664
 
665
+ const setupUpgradeStream = (stream) => {
666
+ let responseReceived = false
667
+
668
+ const removeUpgradeStreamListeners = () => {
669
+ stream.off('response', onUpgradeResponse)
670
+ stream.off('error', onUpgradeStreamError)
671
+ stream.off('end', onUpgradeStreamEnd)
672
+ stream.off('timeout', onUpgradeStreamTimeout)
673
+ stream.off('error', noop)
674
+ }
675
+
676
+ const releaseUpgradeStream = () => {
677
+ if (request[kRequestStream] === stream) {
678
+ detachRequestFromStream(request)
679
+ }
680
+
681
+ removeUpgradeStreamListeners()
682
+
683
+ if (!stream.destroyed && !stream.closed) {
684
+ stream.once('error', noop)
685
+ }
686
+ }
687
+
688
+ const failUpgradeStream = (err) => {
689
+ if (responseReceived || request.aborted || request.completed) {
690
+ return
691
+ }
692
+
693
+ releaseUpgradeStream()
694
+ abort(err, true)
695
+ }
696
+
697
+ const onUpgradeStreamError = () => {
698
+ if (typeof stream.rstCode === 'number' && stream.rstCode !== 0) {
699
+ failUpgradeStream(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
700
+ } else {
701
+ failUpgradeStream(new InformationalError('HTTP/2: stream errored before response headers'))
702
+ }
703
+ }
704
+
705
+ const onUpgradeStreamEnd = () => {
706
+ failUpgradeStream(new InformationalError('HTTP/2: stream half-closed (remote)'))
707
+ }
708
+
709
+ const onUpgradeStreamTimeout = () => {
710
+ failUpgradeStream(new InformationalError(`HTTP/2: "stream timeout after ${requestTimeout}"`))
711
+ }
712
+
713
+ const onUpgradeResponse = (headers, _flags) => {
714
+ responseReceived = true
715
+
716
+ const statusCode = headers[HTTP2_HEADER_STATUS]
717
+ delete headers[HTTP2_HEADER_STATUS]
718
+
719
+ request.onRequestUpgrade(statusCode, headers, stream)
720
+
721
+ if (request.aborted || request.completed) {
722
+ return
723
+ }
724
+
725
+ removeUpgradeStreamListeners()
726
+ detachRequestFromStream(request)
727
+ finalizeRequest()
728
+ }
729
+
730
+ bindRequestToStream(request, stream, releaseUpgradeStream)
731
+ stream.once('response', onUpgradeResponse)
732
+ stream.on('error', onUpgradeStreamError)
733
+ stream.once('end', onUpgradeStreamEnd)
734
+ stream.on('timeout', onUpgradeStreamTimeout)
735
+ stream[kHTTP2Session] = session
736
+ stream[kRequestStreamOnCloseError] = failUpgradeStream
737
+ stream.once('close', onUpgradeStreamClose)
738
+
739
+ ++session[kOpenStreams]
740
+ stream.setTimeout(requestTimeout)
741
+ }
742
+
509
743
  if (upgrade === 'websocket') {
510
744
  // We cannot upgrade to websocket if extended CONNECT protocol is not supported
511
745
  if (session[kEnableConnectProtocol] === false) {
@@ -528,33 +762,13 @@ function writeH2 (client, request) {
528
762
  headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
529
763
  }
530
764
 
531
- stream = session.request(headers, { endStream: false, signal })
765
+ stream = requestStream(headers, { endStream: false, signal })
766
+ if (stream == null) {
767
+ session.unref()
768
+ return false
769
+ }
532
770
  stream[kHTTP2Stream] = true
533
-
534
- stream.once('response', (headers, _flags) => {
535
- const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
536
-
537
- request.onRequestUpgrade(statusCode, parseH2Headers(realHeaders), stream)
538
-
539
- ++session[kOpenStreams]
540
- client[kQueue][client[kRunningIdx]++] = null
541
- })
542
-
543
- stream.on('error', () => {
544
- if (stream.rstCode === NGHTTP2_REFUSED_STREAM || stream.rstCode === NGHTTP2_CANCEL) {
545
- // NGHTTP2_REFUSED_STREAM (7) or NGHTTP2_CANCEL (8)
546
- // We do not treat those as errors as the server might
547
- // not support websockets and refuse the stream
548
- abort(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
549
- }
550
- })
551
-
552
- stream.once('close', () => {
553
- session[kOpenStreams] -= 1
554
- if (session[kOpenStreams] === 0) session.unref()
555
- })
556
-
557
- stream.setTimeout(requestTimeout)
771
+ setupUpgradeStream(stream)
558
772
  return true
559
773
  }
560
774
 
@@ -563,20 +777,13 @@ function writeH2 (client, request) {
563
777
  // will create a new stream. We trigger a request to create the stream and wait until
564
778
  // `ready` event is triggered
565
779
  // We disabled endStream to allow the user to write to the stream
566
- stream = session.request(headers, { endStream: false, signal })
780
+ stream = requestStream(headers, { endStream: false, signal })
781
+ if (stream == null) {
782
+ session.unref()
783
+ return false
784
+ }
567
785
  stream[kHTTP2Stream] = true
568
- stream.on('response', headers => {
569
- const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
570
-
571
- request.onRequestUpgrade(statusCode, parseH2Headers(realHeaders), stream)
572
- ++session[kOpenStreams]
573
- client[kQueue][client[kRunningIdx]++] = null
574
- })
575
- stream.once('close', () => {
576
- session[kOpenStreams] -= 1
577
- if (session[kOpenStreams] === 0) session.unref()
578
- })
579
- stream.setTimeout(requestTimeout)
786
+ setupUpgradeStream(stream)
580
787
 
581
788
  return true
582
789
  }
@@ -622,7 +829,7 @@ function writeH2 (client, request) {
622
829
  contentLength = request.contentLength
623
830
  }
624
831
 
625
- if (!expectsPayload) {
832
+ if (contentLength === 0 && !expectsPayload) {
626
833
  // https://tools.ietf.org/html/rfc7230#section-3.3.2
627
834
  // A user agent SHOULD NOT send a Content-Length header field when
628
835
  // the request message does not contain a payload body and the method
@@ -658,21 +865,25 @@ function writeH2 (client, request) {
658
865
  }
659
866
 
660
867
  // TODO(metcoder95): add support for sending trailers
661
- const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null
868
+ const shouldEndStream = body === null
662
869
  if (expectContinue) {
663
870
  headers[HTTP2_HEADER_EXPECT] = '100-continue'
664
- stream = session.request(headers, { endStream: shouldEndStream, signal })
871
+ stream = requestStream(headers, { endStream: shouldEndStream, signal })
872
+ if (stream == null) {
873
+ return false
874
+ }
665
875
  stream[kHTTP2Stream] = true
666
-
667
- stream.once('continue', writeBodyH2)
876
+ bindRequestToStream(request, stream, null)
668
877
  } else {
669
- stream = session.request(headers, {
878
+ stream = requestStream(headers, {
670
879
  endStream: shouldEndStream,
671
880
  signal
672
881
  })
882
+ if (stream == null) {
883
+ return false
884
+ }
673
885
  stream[kHTTP2Stream] = true
674
-
675
- writeBodyH2()
886
+ bindRequestToStream(request, stream, null)
676
887
  }
677
888
 
678
889
  // Increment counter as we have new streams open
@@ -681,9 +892,46 @@ function writeH2 (client, request) {
681
892
 
682
893
  // Track whether we received a response (headers)
683
894
  let responseReceived = false
895
+ const onData = (chunk) => {
896
+ if (request.aborted || request.completed) {
897
+ return
898
+ }
684
899
 
685
- stream.once('response', headers => {
686
- const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
900
+ if (request.onResponseData(chunk) === false) {
901
+ stream.pause()
902
+ }
903
+ }
904
+
905
+ const removeRequestStreamListeners = () => {
906
+ stream.off('error', noop)
907
+ stream.off('continue', writeBodyH2)
908
+ stream.off('response', onResponse)
909
+ stream.off('end', onEnd)
910
+ stream.off('error', onError)
911
+ stream.off('frameError', onFrameError)
912
+ stream.off('aborted', onAborted)
913
+ stream.off('timeout', onTimeout)
914
+ stream.off('trailers', onTrailers)
915
+ stream.off('data', onData)
916
+ }
917
+
918
+ const releaseRequestStream = () => {
919
+ if (request[kRequestStream] === stream) {
920
+ detachRequestFromStream(request)
921
+ }
922
+
923
+ removeRequestStreamListeners()
924
+
925
+ if (!stream.destroyed && !stream.closed) {
926
+ stream.once('error', noop)
927
+ }
928
+ }
929
+
930
+ const onResponse = (headers) => {
931
+ stream.off('response', onResponse)
932
+
933
+ const statusCode = headers[HTTP2_HEADER_STATUS]
934
+ delete headers[HTTP2_HEADER_STATUS]
687
935
  request.onResponseStarted()
688
936
  responseReceived = true
689
937
 
@@ -693,86 +941,91 @@ function writeH2 (client, request) {
693
941
  // for those scenarios, best effort is to destroy the stream immediately
694
942
  // as there's no value to keep it open.
695
943
  if (request.aborted) {
696
- stream.removeAllListeners('data')
944
+ releaseRequestStream()
697
945
  return
698
946
  }
699
947
 
700
- if (request.onResponseStart(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) {
948
+ if (request.onResponseStart(Number(statusCode), headers, stream.resume.bind(stream), '') === false) {
701
949
  stream.pause()
702
950
  }
703
951
 
704
- stream.on('data', (chunk) => {
705
- if (request.aborted || request.completed) {
706
- return
707
- }
952
+ stream.on('data', onData)
953
+ }
708
954
 
709
- if (request.onResponseData(chunk) === false) {
710
- stream.pause()
711
- }
712
- })
713
- })
955
+ const onEnd = () => {
956
+ stream.off('end', onEnd)
714
957
 
715
- stream.once('end', () => {
716
- stream.removeAllListeners('data')
958
+ releaseRequestStream()
717
959
  // If we received a response, this is a normal completion
718
960
  if (responseReceived) {
719
961
  if (!request.aborted && !request.completed) {
720
962
  request.onResponseEnd({})
721
963
  }
722
964
 
723
- client[kQueue][client[kRunningIdx]++] = null
724
- client[kResume]()
965
+ finalizeRequest()
725
966
  } else {
726
967
  // Stream ended without receiving a response - this is an error
727
968
  // (e.g., server destroyed the stream before sending headers)
728
- abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
729
- client[kQueue][client[kRunningIdx]++] = null
730
- client[kPendingIdx] = client[kRunningIdx]
731
- client[kResume]()
969
+ abort(new InformationalError('HTTP/2: stream half-closed (remote)'), true)
732
970
  }
733
- })
971
+ }
734
972
 
735
- stream.once('close', () => {
736
- stream.removeAllListeners('data')
737
- session[kOpenStreams] -= 1
738
- if (session[kOpenStreams] === 0) {
739
- session.unref()
740
- }
741
- })
973
+ stream[kHTTP2Session] = session
974
+ stream[kRequestStreamOnData] = onData
975
+ stream.once('close', onRequestStreamClose)
742
976
 
743
- stream.once('error', function (err) {
744
- stream.removeAllListeners('data')
977
+ const onError = function (err) {
978
+ stream.off('error', onError)
979
+
980
+ releaseRequestStream()
745
981
  abort(err)
746
- })
982
+ }
747
983
 
748
- stream.once('frameError', (type, code) => {
749
- stream.removeAllListeners('data')
750
- abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
751
- })
984
+ const onFrameError = (type, code) => {
985
+ stream.off('frameError', onFrameError)
752
986
 
753
- stream.on('aborted', () => {
754
- stream.removeAllListeners('data')
755
- })
987
+ releaseRequestStream()
988
+ abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
989
+ }
756
990
 
757
- stream.on('timeout', () => {
758
- const err = new InformationalError(`HTTP/2: "stream timeout after ${requestTimeout}"`)
759
- stream.removeAllListeners('data')
760
- session[kOpenStreams] -= 1
991
+ const onAborted = () => {
992
+ stream.off('data', onData)
993
+ }
761
994
 
762
- if (session[kOpenStreams] === 0) {
763
- session.unref()
764
- }
995
+ const onTimeout = () => {
996
+ releaseRequestStream()
765
997
 
998
+ const err = new InformationalError(`HTTP/2: "stream timeout after ${requestTimeout}"`)
766
999
  abort(err)
767
- })
1000
+ }
1001
+
1002
+ const onTrailers = (trailers) => {
1003
+ stream.off('trailers', onTrailers)
768
1004
 
769
- stream.once('trailers', trailers => {
770
1005
  if (request.aborted || request.completed) {
771
1006
  return
772
1007
  }
773
1008
 
1009
+ releaseRequestStream()
774
1010
  request.onResponseEnd(trailers)
775
- })
1011
+ finalizeRequest()
1012
+ }
1013
+
1014
+ bindRequestToStream(request, stream, releaseRequestStream)
1015
+ if (expectContinue) {
1016
+ stream.once('continue', writeBodyH2)
1017
+ }
1018
+ stream.once('response', onResponse)
1019
+ stream.once('end', onEnd)
1020
+ stream.once('error', onError)
1021
+ stream.once('frameError', onFrameError)
1022
+ stream.on('aborted', onAborted)
1023
+ stream.on('timeout', onTimeout)
1024
+ stream.once('trailers', onTrailers)
1025
+
1026
+ if (!expectContinue) {
1027
+ writeBodyH2()
1028
+ }
776
1029
 
777
1030
  return true
778
1031