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.
- package/README.md +67 -23
- package/docs/docs/api/Agent.md +3 -0
- package/docs/docs/api/Client.md +43 -5
- package/docs/docs/api/Connector.md +1 -0
- package/docs/docs/api/Dispatcher.md +7 -0
- package/docs/docs/api/Errors.md +12 -0
- package/docs/docs/api/EventSource.md +50 -3
- package/docs/docs/api/Fetch.md +3 -1
- package/docs/docs/api/GlobalInstallation.md +7 -5
- package/docs/docs/api/H2CClient.md +2 -2
- package/docs/docs/api/Pool.md +3 -0
- package/docs/docs/api/RedirectHandler.md +4 -1
- package/docs/docs/api/SnapshotAgent.md +23 -0
- package/lib/api/api-pipeline.js +4 -0
- package/lib/api/api-stream.js +51 -5
- package/lib/core/connect.js +29 -4
- package/lib/core/symbols.js +1 -0
- package/lib/core/util.js +10 -8
- package/lib/dispatcher/client-h1.js +59 -18
- package/lib/dispatcher/client-h2.js +418 -298
- package/lib/dispatcher/client.js +25 -4
- package/lib/dispatcher/pool-base.js +21 -3
- package/lib/dispatcher/pool.js +23 -0
- package/lib/dispatcher/proxy-agent.js +21 -4
- package/lib/dispatcher/round-robin-pool.js +26 -0
- package/lib/dispatcher/socks5-proxy-agent.js +19 -19
- package/lib/handler/redirect-handler.js +36 -11
- package/lib/handler/retry-handler.js +14 -0
- package/lib/interceptor/redirect.js +3 -3
- package/lib/mock/mock-call-history.js +1 -1
- package/lib/mock/mock-utils.js +3 -1
- package/lib/mock/snapshot-agent.js +11 -1
- package/lib/mock/snapshot-recorder.js +38 -3
- package/lib/web/fetch/body.js +2 -7
- package/lib/web/fetch/formdata.js +21 -2
- package/lib/web/fetch/index.js +19 -3
- package/lib/web/fetch/request.js +32 -3
- package/package.json +4 -4
- package/types/client.d.ts +7 -7
- package/types/connector.d.ts +1 -0
- package/types/dispatcher.d.ts +0 -2
- package/types/fetch.d.ts +4 -1
- package/types/formdata.d.ts +0 -6
- package/types/interceptors.d.ts +1 -1
- 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
|
|
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
|
|
520
|
-
this[
|
|
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
|
|
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 (
|
|
558
|
-
headers[key] = Array.isArray(
|
|
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 (
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
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
|
|
635
|
-
|
|
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
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
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
|
|
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(
|
|
974
|
+
stream.setTimeout(headersTimeout)
|
|
892
975
|
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
|
|
901
|
-
|
|
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
|
-
|
|
906
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
detachRequestFromStream(request)
|
|
921
|
-
}
|
|
995
|
+
return true
|
|
996
|
+
}
|
|
922
997
|
|
|
923
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1011
|
+
function releaseRequestStream (stream) {
|
|
1012
|
+
if (stream == null) {
|
|
1013
|
+
return
|
|
928
1014
|
}
|
|
929
1015
|
|
|
930
|
-
const
|
|
931
|
-
|
|
1016
|
+
const state = stream[kRequestStreamState]
|
|
1017
|
+
if (state == null) {
|
|
1018
|
+
return
|
|
1019
|
+
}
|
|
932
1020
|
|
|
933
|
-
|
|
934
|
-
delete headers[HTTP2_HEADER_STATUS]
|
|
935
|
-
request.onResponseStarted()
|
|
936
|
-
responseReceived = true
|
|
1021
|
+
const { request } = state
|
|
937
1022
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
949
|
-
stream.pause()
|
|
950
|
-
}
|
|
1027
|
+
removeRequestStreamListeners(stream)
|
|
951
1028
|
|
|
952
|
-
|
|
1029
|
+
if (!stream.destroyed && !stream.closed) {
|
|
1030
|
+
stream.once('error', noop)
|
|
953
1031
|
}
|
|
1032
|
+
}
|
|
954
1033
|
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
|
|
966
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1042
|
+
if (request.onResponseData(chunk) === false) {
|
|
1043
|
+
stream.pause()
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
976
1046
|
|
|
977
|
-
|
|
978
|
-
|
|
1047
|
+
function onResponse (headers) {
|
|
1048
|
+
const stream = this
|
|
1049
|
+
const state = stream[kRequestStreamState]
|
|
1050
|
+
const { request } = state
|
|
979
1051
|
|
|
980
|
-
|
|
981
|
-
abort(err)
|
|
982
|
-
}
|
|
1052
|
+
stream.off('response', onResponse)
|
|
983
1053
|
|
|
984
|
-
const
|
|
985
|
-
|
|
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
|
-
|
|
988
|
-
|
|
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
|
-
|
|
992
|
-
stream.
|
|
1070
|
+
if (request.onResponseStart(Number(statusCode), headers, stream.resume.bind(stream), '') === false) {
|
|
1071
|
+
stream.pause()
|
|
993
1072
|
}
|
|
994
1073
|
|
|
995
|
-
|
|
996
|
-
|
|
1074
|
+
stream.on('data', onData)
|
|
1075
|
+
}
|
|
997
1076
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1077
|
+
function onEnd () {
|
|
1078
|
+
const stream = this
|
|
1079
|
+
const state = stream[kRequestStreamState]
|
|
1080
|
+
const { request } = state
|
|
1001
1081
|
|
|
1002
|
-
|
|
1003
|
-
stream.off('trailers', onTrailers)
|
|
1082
|
+
stream.off('end', onEnd)
|
|
1004
1083
|
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1146
|
+
releaseRequestStream(stream)
|
|
1147
|
+
request.onResponseEnd(trailers)
|
|
1148
|
+
state.finalizeRequest()
|
|
1149
|
+
}
|
|
1031
1150
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
|
1056
|
-
|
|
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()
|