undici 8.2.0 → 8.4.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 (45) hide show
  1. package/README.md +67 -23
  2. package/docs/docs/api/Agent.md +3 -0
  3. package/docs/docs/api/Client.md +43 -5
  4. package/docs/docs/api/Connector.md +1 -0
  5. package/docs/docs/api/Dispatcher.md +7 -0
  6. package/docs/docs/api/Errors.md +12 -0
  7. package/docs/docs/api/EventSource.md +50 -3
  8. package/docs/docs/api/Fetch.md +3 -1
  9. package/docs/docs/api/GlobalInstallation.md +7 -5
  10. package/docs/docs/api/H2CClient.md +2 -2
  11. package/docs/docs/api/Pool.md +3 -0
  12. package/docs/docs/api/RedirectHandler.md +4 -1
  13. package/docs/docs/api/SnapshotAgent.md +23 -0
  14. package/lib/api/api-pipeline.js +4 -0
  15. package/lib/api/api-stream.js +51 -5
  16. package/lib/core/connect.js +29 -4
  17. package/lib/core/symbols.js +1 -0
  18. package/lib/core/util.js +10 -8
  19. package/lib/dispatcher/client-h1.js +59 -18
  20. package/lib/dispatcher/client-h2.js +418 -298
  21. package/lib/dispatcher/client.js +25 -4
  22. package/lib/dispatcher/pool-base.js +21 -3
  23. package/lib/dispatcher/pool.js +23 -0
  24. package/lib/dispatcher/proxy-agent.js +21 -4
  25. package/lib/dispatcher/round-robin-pool.js +26 -0
  26. package/lib/dispatcher/socks5-proxy-agent.js +19 -19
  27. package/lib/handler/redirect-handler.js +36 -11
  28. package/lib/handler/retry-handler.js +14 -0
  29. package/lib/interceptor/redirect.js +3 -3
  30. package/lib/mock/mock-call-history.js +1 -1
  31. package/lib/mock/mock-utils.js +3 -1
  32. package/lib/mock/snapshot-agent.js +11 -1
  33. package/lib/mock/snapshot-recorder.js +38 -3
  34. package/lib/web/fetch/body.js +2 -7
  35. package/lib/web/fetch/formdata.js +21 -2
  36. package/lib/web/fetch/index.js +19 -3
  37. package/lib/web/fetch/request.js +32 -3
  38. package/package.json +4 -4
  39. package/types/client.d.ts +7 -7
  40. package/types/connector.d.ts +1 -0
  41. package/types/dispatcher.d.ts +0 -2
  42. package/types/fetch.d.ts +4 -1
  43. package/types/formdata.d.ts +0 -6
  44. package/types/interceptors.d.ts +1 -1
  45. package/types/snapshot-agent.d.ts +4 -0
@@ -8,7 +8,9 @@ const {
8
8
  RequestAbortedError,
9
9
  SocketError,
10
10
  InformationalError,
11
- InvalidArgumentError
11
+ InvalidArgumentError,
12
+ HeadersTimeoutError,
13
+ BodyTimeoutError
12
14
  } = require('../core/errors.js')
13
15
  const {
14
16
  kUrl,
@@ -28,10 +30,12 @@ const {
28
30
  kHTTP2Session,
29
31
  kHTTP2InitialWindowSize,
30
32
  kHTTP2ConnectionWindowSize,
33
+ kHostAuthority,
31
34
  kResume,
32
35
  kSize,
33
36
  kHTTPContext,
34
37
  kClosed,
38
+ kHeadersTimeout,
35
39
  kBodyTimeout,
36
40
  kEnableConnectProtocol,
37
41
  kRemoteSettings,
@@ -44,8 +48,7 @@ const kOpenStreams = Symbol('open streams')
44
48
  const kRequestStreamId = Symbol('request stream id')
45
49
  const kRequestStream = Symbol('request stream')
46
50
  const kRequestStreamCleanup = Symbol('request stream cleanup')
47
- const kRequestStreamOnData = Symbol('request stream on data')
48
- const kRequestStreamOnCloseError = Symbol('request stream on close error')
51
+ const kRequestStreamState = Symbol('request stream state')
49
52
  const kReceivedGoAway = Symbol('received goaway')
50
53
 
51
54
  let extractBody
@@ -81,6 +84,29 @@ function getGoAwayError (session, errorCode) {
81
84
  : new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(session[kSocket])))
82
85
  }
83
86
 
87
+ function resetHttp2Session (session, err) {
88
+ const client = session[kClient]
89
+ const socket = session[kSocket]
90
+
91
+ if (client[kHTTP2Session] === session) {
92
+ client[kSocket] = null
93
+ client[kHTTPContext] = null
94
+ client[kHTTP2Session] = null
95
+ }
96
+
97
+ if (socket != null && socket[kError] == null) {
98
+ socket[kError] = err
99
+ }
100
+
101
+ if (!session.closed && !session.destroyed) {
102
+ try {
103
+ session.destroy(err)
104
+ } catch {}
105
+ }
106
+
107
+ util.destroy(socket, err)
108
+ }
109
+
84
110
  function getGoAwayPendingIdx (client, lastStreamID) {
85
111
  const maxAcceptedStreamID = Number.isInteger(lastStreamID) ? lastStreamID : Number.MAX_SAFE_INTEGER
86
112
 
@@ -107,8 +133,9 @@ function detachRequestFromStream (request) {
107
133
 
108
134
  function bindRequestToStream (request, stream, cleanup) {
109
135
  const previousCleanup = request[kRequestStreamCleanup]
136
+ const previousStream = request[kRequestStream]
110
137
  detachRequestFromStream(request)
111
- previousCleanup?.()
138
+ previousCleanup?.(previousStream)
112
139
  request[kRequestStreamId] = stream.id
113
140
  request[kRequestStream] = stream
114
141
  request[kRequestStreamCleanup] = cleanup
@@ -116,8 +143,13 @@ function bindRequestToStream (request, stream, cleanup) {
116
143
 
117
144
  function clearRequestStream (request) {
118
145
  const cleanup = request[kRequestStreamCleanup]
146
+ const stream = request[kRequestStream]
119
147
  detachRequestFromStream(request)
120
- cleanup?.()
148
+ cleanup?.(stream)
149
+ }
150
+
151
+ function requeueUnsentRequest (client, request) {
152
+ client[kQueue].splice(client[kPendingIdx] + 1, 0, request)
121
153
  }
122
154
 
123
155
  function canRetryRequestAfterGoAway (request) {
@@ -516,20 +548,18 @@ function closeStreamSession (stream) {
516
548
  function onUpgradeStreamClose () {
517
549
  this.off('error', noop)
518
550
 
519
- const failUpgradeStream = this[kRequestStreamOnCloseError]
520
- this[kRequestStreamOnCloseError] = null
551
+ const state = this[kRequestStreamState]
552
+ this[kRequestStreamState] = null
521
553
 
522
- failUpgradeStream(new InformationalError('HTTP/2: stream closed before response headers'))
554
+ failUpgradeStream(state, new InformationalError('HTTP/2: stream closed before response headers'))
523
555
  closeStreamSession(this)
524
556
  }
525
557
 
526
558
  function onRequestStreamClose () {
527
- const onData = this[kRequestStreamOnData]
528
-
529
- this[kRequestStreamOnData] = null
530
559
  this.off('data', onData)
531
560
  this.off('error', noop)
532
561
  closeStreamSession(this)
562
+ this[kRequestStreamState] = null
533
563
  }
534
564
 
535
565
  // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
@@ -537,25 +567,17 @@ function shouldSendContentLength (method) {
537
567
  return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
538
568
  }
539
569
 
540
- function writeH2 (client, request) {
541
- const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout]
542
- const session = client[kHTTP2Session]
543
- const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
544
- let { body } = request
545
-
546
- if (upgrade != null && upgrade !== 'websocket') {
547
- util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
548
- return false
549
- }
550
-
570
+ function buildRequestHeaders (reqHeaders) {
551
571
  const headers = {}
572
+
552
573
  for (let n = 0; n < reqHeaders.length; n += 2) {
553
574
  const key = reqHeaders[n + 0]
554
575
  const val = reqHeaders[n + 1]
576
+ const current = headers[key]
555
577
 
556
578
  if (key === 'cookie') {
557
- if (headers[key] != null) {
558
- headers[key] = Array.isArray(headers[key]) ? (headers[key].push(val), headers[key]) : [headers[key], val]
579
+ if (current != null) {
580
+ headers[key] = Array.isArray(current) ? (current.push(val), current) : [current, val]
559
581
  } else {
560
582
  headers[key] = val
561
583
  }
@@ -563,27 +585,141 @@ function writeH2 (client, request) {
563
585
  continue
564
586
  }
565
587
 
566
- if (Array.isArray(val)) {
567
- for (let i = 0; i < val.length; i++) {
568
- if (headers[key]) {
569
- headers[key] += `, ${val[i]}`
570
- } else {
571
- headers[key] = val[i]
572
- }
573
- }
574
- } else if (headers[key]) {
575
- headers[key] += `, ${val}`
576
- } else {
577
- headers[key] = val
588
+ if (typeof val === 'string') {
589
+ headers[key] = current ? `${current}, ${val}` : val
590
+ continue
591
+ }
592
+
593
+ for (let i = 0; i < val.length; i++) {
594
+ headers[key] = headers[key] ? `${headers[key]}, ${val[i]}` : val[i]
578
595
  }
579
596
  }
580
597
 
598
+ return headers
599
+ }
600
+
601
+ function removeUpgradeStreamListeners (stream) {
602
+ stream.off('response', onUpgradeResponse)
603
+ stream.off('error', onUpgradeStreamError)
604
+ stream.off('end', onUpgradeStreamEnd)
605
+ stream.off('timeout', onUpgradeStreamTimeout)
606
+ stream.off('error', noop)
607
+ }
608
+
609
+ function releaseUpgradeStream (stream) {
610
+ if (stream == null) {
611
+ return
612
+ }
613
+
614
+ const state = stream[kRequestStreamState]
615
+ if (state == null) {
616
+ return
617
+ }
618
+
619
+ const { request } = state
620
+
621
+ if (request[kRequestStream] === stream) {
622
+ detachRequestFromStream(request)
623
+ }
624
+
625
+ removeUpgradeStreamListeners(stream)
626
+
627
+ if (!stream.destroyed && !stream.closed) {
628
+ stream.once('error', noop)
629
+ }
630
+ }
631
+
632
+ function failUpgradeStream (state, err) {
633
+ if (state == null) {
634
+ return
635
+ }
636
+
637
+ const { request } = state
638
+ if (state.responseReceived || request.aborted || request.completed) {
639
+ return
640
+ }
641
+
642
+ releaseUpgradeStream(state.stream)
643
+ state.abort(err, true)
644
+ }
645
+
646
+ function onUpgradeStreamError () {
647
+ const state = this[kRequestStreamState]
648
+
649
+ if (typeof this.rstCode === 'number' && this.rstCode !== 0) {
650
+ failUpgradeStream(state, new InformationalError(`HTTP/2: "stream error" received - code ${this.rstCode}`))
651
+ } else {
652
+ failUpgradeStream(state, new InformationalError('HTTP/2: stream errored before response headers'))
653
+ }
654
+ }
655
+
656
+ function onUpgradeStreamEnd () {
657
+ failUpgradeStream(this[kRequestStreamState], new InformationalError('HTTP/2: stream half-closed (remote)'))
658
+ }
659
+
660
+ function onUpgradeStreamTimeout () {
661
+ const state = this[kRequestStreamState]
662
+ failUpgradeStream(state, new InformationalError(`HTTP/2: "stream timeout after ${state.headersTimeout}"`))
663
+ }
664
+
665
+ function onUpgradeResponse (headers, _flags) {
666
+ const stream = this
667
+ const state = stream[kRequestStreamState]
668
+ const { request } = state
669
+
670
+ state.responseReceived = true
671
+
672
+ const statusCode = headers[HTTP2_HEADER_STATUS]
673
+ delete headers[HTTP2_HEADER_STATUS]
674
+
675
+ request.onRequestUpgrade(statusCode, headers, stream)
676
+
677
+ if (request.aborted || request.completed) {
678
+ return
679
+ }
680
+
681
+ removeUpgradeStreamListeners(stream)
682
+ detachRequestFromStream(request)
683
+ state.finalizeRequest()
684
+ }
685
+
686
+ function setupUpgradeStream (stream, state) {
687
+ const { request, headersTimeout, session } = state
688
+
689
+ stream[kHTTP2Stream] = true
690
+ stream[kHTTP2Session] = session
691
+ stream[kRequestStreamState] = state
692
+ state.stream = stream
693
+
694
+ bindRequestToStream(request, stream, releaseUpgradeStream)
695
+ stream.once('response', onUpgradeResponse)
696
+ stream.on('error', onUpgradeStreamError)
697
+ stream.once('end', onUpgradeStreamEnd)
698
+ stream.on('timeout', onUpgradeStreamTimeout)
699
+ stream.once('close', onUpgradeStreamClose)
700
+
701
+ ++session[kOpenStreams]
702
+ stream.setTimeout(headersTimeout)
703
+ }
704
+
705
+ function writeH2 (client, request) {
706
+ const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout]
707
+ const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout]
708
+ const session = client[kHTTP2Session]
709
+ const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
710
+ let { body } = request
711
+
712
+ if (upgrade != null && upgrade !== 'websocket') {
713
+ util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
714
+ return false
715
+ }
716
+
717
+ const headers = buildRequestHeaders(reqHeaders)
718
+
581
719
  /** @type {import('node:http2').ClientHttp2Stream} */
582
720
  let stream = null
583
721
 
584
- const { hostname, port } = client[kUrl]
585
-
586
- headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
722
+ headers[HTTP2_HEADER_AUTHORITY] = host || client[kHostAuthority]
587
723
  headers[HTTP2_HEADER_METHOD] = method
588
724
 
589
725
  let requestFinalized = false
@@ -631,8 +767,14 @@ function writeH2 (client, request) {
631
767
  try {
632
768
  return session.request(headers, options)
633
769
  } catch (err) {
634
- if (err?.code !== 'ERR_HTTP2_INVALID_CONNECTION_HEADERS') {
635
- throw err
770
+ if (err?.code === 'ERR_HTTP2_INVALID_SESSION') {
771
+ const wrappedErr = new SocketError(err.message, util.getSocketInfo(session[kSocket]))
772
+ wrappedErr.cause = err
773
+ session[kError] = wrappedErr
774
+ resetHttp2Session(session, wrappedErr)
775
+ requeueUnsentRequest(client, request)
776
+
777
+ return null
636
778
  }
637
779
 
638
780
  const wrappedErr = new InformationalError(err.message, { cause: err })
@@ -662,82 +804,15 @@ function writeH2 (client, request) {
662
804
  if (upgrade || method === 'CONNECT') {
663
805
  session.ref()
664
806
 
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)
807
+ const upgradeState = {
808
+ abort,
809
+ finalizeRequest,
810
+ request,
811
+ headersTimeout,
812
+ bodyTimeout,
813
+ responseReceived: false,
814
+ session,
815
+ stream: null
741
816
  }
742
817
 
743
818
  if (upgrade === 'websocket') {
@@ -767,8 +842,7 @@ function writeH2 (client, request) {
767
842
  session.unref()
768
843
  return false
769
844
  }
770
- stream[kHTTP2Stream] = true
771
- setupUpgradeStream(stream)
845
+ setupUpgradeStream(stream, upgradeState)
772
846
  return true
773
847
  }
774
848
 
@@ -782,8 +856,7 @@ function writeH2 (client, request) {
782
856
  session.unref()
783
857
  return false
784
858
  }
785
- stream[kHTTP2Stream] = true
786
- setupUpgradeStream(stream)
859
+ setupUpgradeStream(stream, upgradeState)
787
860
 
788
861
  return true
789
862
  }
@@ -805,7 +878,10 @@ function writeH2 (client, request) {
805
878
  const expectsPayload = (
806
879
  method === 'PUT' ||
807
880
  method === 'POST' ||
808
- method === 'PATCH'
881
+ method === 'PATCH' ||
882
+ method === 'QUERY' ||
883
+ method === 'PROPFIND' ||
884
+ method === 'PROPPATCH'
809
885
  )
810
886
 
811
887
  if (body && typeof body.read === 'function') {
@@ -865,230 +941,254 @@ function writeH2 (client, request) {
865
941
  }
866
942
 
867
943
  // TODO(metcoder95): add support for sending trailers
868
- const shouldEndStream = body === null
944
+ const shouldEndStream = body === null || contentLength === 0
945
+ const state = {
946
+ abort,
947
+ body,
948
+ client,
949
+ contentLength,
950
+ expectsPayload,
951
+ finalizeRequest,
952
+ request,
953
+ headersTimeout,
954
+ bodyTimeout,
955
+ responseReceived: false,
956
+ session,
957
+ stream: null
958
+ }
959
+
869
960
  if (expectContinue) {
870
961
  headers[HTTP2_HEADER_EXPECT] = '100-continue'
871
- stream = requestStream(headers, { endStream: shouldEndStream, signal })
872
- if (stream == null) {
873
- return false
874
- }
875
- stream[kHTTP2Stream] = true
876
- bindRequestToStream(request, stream, null)
877
- } else {
878
- stream = requestStream(headers, {
879
- endStream: shouldEndStream,
880
- signal
881
- })
882
- if (stream == null) {
883
- return false
884
- }
885
- stream[kHTTP2Stream] = true
886
- bindRequestToStream(request, stream, null)
887
962
  }
888
963
 
964
+ stream = requestStream(headers, { endStream: shouldEndStream, signal })
965
+ if (stream == null) {
966
+ return false
967
+ }
968
+ stream[kHTTP2Stream] = true
969
+ stream[kRequestStreamState] = state
970
+ state.stream = stream
971
+
889
972
  // Increment counter as we have new streams open
890
973
  ++session[kOpenStreams]
891
- stream.setTimeout(requestTimeout)
974
+ stream.setTimeout(headersTimeout)
892
975
 
893
- // Track whether we received a response (headers)
894
- let responseReceived = false
895
- const onData = (chunk) => {
896
- if (request.aborted || request.completed) {
897
- return
898
- }
976
+ stream[kHTTP2Session] = session
977
+ stream.once('close', onRequestStreamClose)
899
978
 
900
- if (request.onResponseData(chunk) === false) {
901
- stream.pause()
902
- }
979
+ bindRequestToStream(request, stream, releaseRequestStream)
980
+ if (expectContinue) {
981
+ stream.once('continue', writeBodyH2)
903
982
  }
983
+ stream.once('response', onResponse)
984
+ stream.once('end', onEnd)
985
+ stream.once('error', onError)
986
+ stream.once('frameError', onFrameError)
987
+ stream.on('aborted', onAborted)
988
+ stream.on('timeout', onTimeout)
989
+ stream.once('trailers', onTrailers)
904
990
 
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)
991
+ if (!expectContinue) {
992
+ writeBodyH2.call(stream)
916
993
  }
917
994
 
918
- const releaseRequestStream = () => {
919
- if (request[kRequestStream] === stream) {
920
- detachRequestFromStream(request)
921
- }
995
+ return true
996
+ }
922
997
 
923
- removeRequestStreamListeners()
998
+ function removeRequestStreamListeners (stream) {
999
+ stream.off('error', noop)
1000
+ stream.off('continue', writeBodyH2)
1001
+ stream.off('response', onResponse)
1002
+ stream.off('end', onEnd)
1003
+ stream.off('error', onError)
1004
+ stream.off('frameError', onFrameError)
1005
+ stream.off('aborted', onAborted)
1006
+ stream.off('timeout', onTimeout)
1007
+ stream.off('trailers', onTrailers)
1008
+ stream.off('data', onData)
1009
+ }
924
1010
 
925
- if (!stream.destroyed && !stream.closed) {
926
- stream.once('error', noop)
927
- }
1011
+ function releaseRequestStream (stream) {
1012
+ if (stream == null) {
1013
+ return
928
1014
  }
929
1015
 
930
- const onResponse = (headers) => {
931
- stream.off('response', onResponse)
1016
+ const state = stream[kRequestStreamState]
1017
+ if (state == null) {
1018
+ return
1019
+ }
932
1020
 
933
- const statusCode = headers[HTTP2_HEADER_STATUS]
934
- delete headers[HTTP2_HEADER_STATUS]
935
- request.onResponseStarted()
936
- responseReceived = true
1021
+ const { request } = state
937
1022
 
938
- // Due to the stream nature, it is possible we face a race condition
939
- // where the stream has been assigned, but the request has been aborted
940
- // the request remains in-flight and headers hasn't been received yet
941
- // for those scenarios, best effort is to destroy the stream immediately
942
- // as there's no value to keep it open.
943
- if (request.aborted) {
944
- releaseRequestStream()
945
- return
946
- }
1023
+ if (request[kRequestStream] === stream) {
1024
+ detachRequestFromStream(request)
1025
+ }
947
1026
 
948
- if (request.onResponseStart(Number(statusCode), headers, stream.resume.bind(stream), '') === false) {
949
- stream.pause()
950
- }
1027
+ removeRequestStreamListeners(stream)
951
1028
 
952
- stream.on('data', onData)
1029
+ if (!stream.destroyed && !stream.closed) {
1030
+ stream.once('error', noop)
953
1031
  }
1032
+ }
954
1033
 
955
- const onEnd = () => {
956
- stream.off('end', onEnd)
957
-
958
- releaseRequestStream()
959
- // If we received a response, this is a normal completion
960
- if (responseReceived) {
961
- if (!request.aborted && !request.completed) {
962
- request.onResponseEnd({})
963
- }
1034
+ function onData (chunk) {
1035
+ const stream = this
1036
+ const { request } = stream[kRequestStreamState]
964
1037
 
965
- finalizeRequest()
966
- } else {
967
- // Stream ended without receiving a response - this is an error
968
- // (e.g., server destroyed the stream before sending headers)
969
- abort(new InformationalError('HTTP/2: stream half-closed (remote)'), true)
970
- }
1038
+ if (request.aborted || request.completed) {
1039
+ return
971
1040
  }
972
1041
 
973
- stream[kHTTP2Session] = session
974
- stream[kRequestStreamOnData] = onData
975
- stream.once('close', onRequestStreamClose)
1042
+ if (request.onResponseData(chunk) === false) {
1043
+ stream.pause()
1044
+ }
1045
+ }
976
1046
 
977
- const onError = function (err) {
978
- stream.off('error', onError)
1047
+ function onResponse (headers) {
1048
+ const stream = this
1049
+ const state = stream[kRequestStreamState]
1050
+ const { request } = state
979
1051
 
980
- releaseRequestStream()
981
- abort(err)
982
- }
1052
+ stream.off('response', onResponse)
983
1053
 
984
- const onFrameError = (type, code) => {
985
- stream.off('frameError', onFrameError)
1054
+ const statusCode = headers[HTTP2_HEADER_STATUS]
1055
+ delete headers[HTTP2_HEADER_STATUS]
1056
+ request.onResponseStarted()
1057
+ state.responseReceived = true
1058
+ stream.setTimeout(state.bodyTimeout)
986
1059
 
987
- releaseRequestStream()
988
- abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
1060
+ // Due to the stream nature, it is possible we face a race condition
1061
+ // where the stream has been assigned, but the request has been aborted
1062
+ // the request remains in-flight and headers hasn't been received yet
1063
+ // for those scenarios, best effort is to destroy the stream immediately
1064
+ // as there's no value to keep it open.
1065
+ if (request.aborted) {
1066
+ releaseRequestStream(stream)
1067
+ return
989
1068
  }
990
1069
 
991
- const onAborted = () => {
992
- stream.off('data', onData)
1070
+ if (request.onResponseStart(Number(statusCode), headers, stream.resume.bind(stream), '') === false) {
1071
+ stream.pause()
993
1072
  }
994
1073
 
995
- const onTimeout = () => {
996
- releaseRequestStream()
1074
+ stream.on('data', onData)
1075
+ }
997
1076
 
998
- const err = new InformationalError(`HTTP/2: "stream timeout after ${requestTimeout}"`)
999
- abort(err)
1000
- }
1077
+ function onEnd () {
1078
+ const stream = this
1079
+ const state = stream[kRequestStreamState]
1080
+ const { request } = state
1001
1081
 
1002
- const onTrailers = (trailers) => {
1003
- stream.off('trailers', onTrailers)
1082
+ stream.off('end', onEnd)
1004
1083
 
1005
- if (request.aborted || request.completed) {
1006
- return
1084
+ releaseRequestStream(stream)
1085
+ // If we received a response, this is a normal completion
1086
+ if (state.responseReceived) {
1087
+ if (!request.aborted && !request.completed) {
1088
+ request.onResponseEnd({})
1007
1089
  }
1008
1090
 
1009
- releaseRequestStream()
1010
- request.onResponseEnd(trailers)
1011
- finalizeRequest()
1091
+ state.finalizeRequest()
1092
+ } else {
1093
+ // Stream ended without receiving a response - this is an error
1094
+ // (e.g., server destroyed the stream before sending headers)
1095
+ state.abort(new InformationalError('HTTP/2: stream half-closed (remote)'), true)
1012
1096
  }
1097
+ }
1013
1098
 
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)
1099
+ function onError (err) {
1100
+ const stream = this
1101
+ const state = stream[kRequestStreamState]
1025
1102
 
1026
- if (!expectContinue) {
1027
- writeBodyH2()
1103
+ stream.off('error', onError)
1104
+
1105
+ releaseRequestStream(stream)
1106
+ state.abort(err)
1107
+ }
1108
+
1109
+ function onFrameError (type, code) {
1110
+ const stream = this
1111
+ const state = stream[kRequestStreamState]
1112
+
1113
+ stream.off('frameError', onFrameError)
1114
+
1115
+ releaseRequestStream(stream)
1116
+ state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
1117
+ }
1118
+
1119
+ function onAborted () {
1120
+ this.off('data', onData)
1121
+ }
1122
+
1123
+ function onTimeout () {
1124
+ const stream = this
1125
+ const state = stream[kRequestStreamState]
1126
+
1127
+ releaseRequestStream(stream)
1128
+
1129
+ const err = state.responseReceived
1130
+ ? new BodyTimeoutError(`HTTP/2: "stream timeout after ${state.bodyTimeout}"`)
1131
+ : new HeadersTimeoutError(`HTTP/2: "headers timeout after ${state.headersTimeout}"`)
1132
+ state.abort(err)
1133
+ }
1134
+
1135
+ function onTrailers (trailers) {
1136
+ const stream = this
1137
+ const state = stream[kRequestStreamState]
1138
+ const { request } = state
1139
+
1140
+ stream.off('trailers', onTrailers)
1141
+
1142
+ if (request.aborted || request.completed) {
1143
+ return
1028
1144
  }
1029
1145
 
1030
- return true
1146
+ releaseRequestStream(stream)
1147
+ request.onResponseEnd(trailers)
1148
+ state.finalizeRequest()
1149
+ }
1031
1150
 
1032
- function writeBodyH2 () {
1033
- if (!body || contentLength === 0) {
1034
- writeBuffer(
1035
- abort,
1036
- stream,
1037
- null,
1038
- client,
1039
- request,
1040
- client[kSocket],
1041
- contentLength,
1042
- expectsPayload
1043
- )
1044
- } else if (util.isBuffer(body)) {
1045
- writeBuffer(
1151
+ function writeBodyH2 () {
1152
+ const stream = this
1153
+ const state = stream[kRequestStreamState]
1154
+ const { abort, body, client, contentLength, expectsPayload, request } = state
1155
+
1156
+ if (!body || contentLength === 0) {
1157
+ writeBuffer(
1158
+ abort,
1159
+ stream,
1160
+ null,
1161
+ client,
1162
+ request,
1163
+ client[kSocket],
1164
+ contentLength,
1165
+ expectsPayload
1166
+ )
1167
+ } else if (util.isBuffer(body)) {
1168
+ writeBuffer(
1169
+ abort,
1170
+ stream,
1171
+ body,
1172
+ client,
1173
+ request,
1174
+ client[kSocket],
1175
+ contentLength,
1176
+ expectsPayload
1177
+ )
1178
+ } else if (util.isBlobLike(body)) {
1179
+ if (typeof body.stream === 'function') {
1180
+ writeIterable(
1046
1181
  abort,
1047
1182
  stream,
1048
- body,
1183
+ body.stream(),
1049
1184
  client,
1050
1185
  request,
1051
1186
  client[kSocket],
1052
1187
  contentLength,
1053
1188
  expectsPayload
1054
1189
  )
1055
- } else if (util.isBlobLike(body)) {
1056
- if (typeof body.stream === 'function') {
1057
- writeIterable(
1058
- abort,
1059
- stream,
1060
- body.stream(),
1061
- client,
1062
- request,
1063
- client[kSocket],
1064
- contentLength,
1065
- expectsPayload
1066
- )
1067
- } else {
1068
- writeBlob(
1069
- abort,
1070
- stream,
1071
- body,
1072
- client,
1073
- request,
1074
- client[kSocket],
1075
- contentLength,
1076
- expectsPayload
1077
- )
1078
- }
1079
- } else if (util.isStream(body)) {
1080
- writeStream(
1081
- abort,
1082
- client[kSocket],
1083
- expectsPayload,
1084
- stream,
1085
- body,
1086
- client,
1087
- request,
1088
- contentLength
1089
- )
1090
- } else if (util.isIterable(body)) {
1091
- writeIterable(
1190
+ } else {
1191
+ writeBlob(
1092
1192
  abort,
1093
1193
  stream,
1094
1194
  body,
@@ -1098,9 +1198,31 @@ function writeH2 (client, request) {
1098
1198
  contentLength,
1099
1199
  expectsPayload
1100
1200
  )
1101
- } else {
1102
- assert(false)
1103
1201
  }
1202
+ } else if (util.isStream(body)) {
1203
+ writeStream(
1204
+ abort,
1205
+ client[kSocket],
1206
+ expectsPayload,
1207
+ stream,
1208
+ body,
1209
+ client,
1210
+ request,
1211
+ contentLength
1212
+ )
1213
+ } else if (util.isIterable(body)) {
1214
+ writeIterable(
1215
+ abort,
1216
+ stream,
1217
+ body,
1218
+ client,
1219
+ request,
1220
+ client[kSocket],
1221
+ contentLength,
1222
+ expectsPayload
1223
+ )
1224
+ } else {
1225
+ assert(false)
1104
1226
  }
1105
1227
  }
1106
1228
 
@@ -1159,8 +1281,6 @@ function writeStream (abort, socket, expectsPayload, h2stream, body, client, req
1159
1281
  }
1160
1282
 
1161
1283
  async function writeBlob (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
1162
- assert(contentLength === body.size, 'blob body must have content length')
1163
-
1164
1284
  try {
1165
1285
  if (contentLength != null && contentLength !== body.size) {
1166
1286
  throw new RequestContentLengthMismatchError()