undici 6.11.1 → 6.12.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/docs/docs/api/Dispatcher.md +6 -0
- package/lib/api/abort-signal.js +4 -1
- package/lib/api/api-connect.js +7 -3
- package/lib/api/api-pipeline.js +6 -4
- package/lib/api/api-request.js +7 -6
- package/lib/api/api-stream.js +7 -7
- package/lib/api/api-upgrade.js +6 -3
- package/lib/core/util.js +41 -8
- package/lib/dispatcher/client-h1.js +63 -68
- package/lib/dispatcher/client-h2.js +52 -68
- package/lib/dispatcher/client.js +7 -13
- package/lib/dispatcher/proxy-agent.js +11 -9
- package/lib/mock/mock-interceptor.js +18 -17
- package/lib/mock/mock-utils.js +5 -5
- package/lib/web/fetch/constants.js +2 -2
- package/lib/web/websocket/receiver.js +2 -3
- package/lib/web/websocket/util.js +31 -2
- package/package.json +12 -8
- package/types/dispatcher.d.ts +1 -1
- package/types/fetch.d.ts +1 -1
|
@@ -969,6 +969,12 @@ Parameters:
|
|
|
969
969
|
* **targets** `Array<Dispatcher>`
|
|
970
970
|
* **error** `Error`
|
|
971
971
|
|
|
972
|
+
Emitted when the dispatcher has been disconnected from the origin.
|
|
973
|
+
|
|
974
|
+
> **Note**: For HTTP/2, this event is also emitted when the dispatcher has received the [GOAWAY Frame](https://webconcepts.info/concepts/http2-frame-type/0x7) with an Error with the message `HTTP/2: "GOAWAY" frame received` and the code `UND_ERR_INFO`.
|
|
975
|
+
> Due to nature of the protocol of using binary frames, it is possible that requests gets hanging as a frame can be received between the `HEADER` and `DATA` frames.
|
|
976
|
+
> It is recommended to handle this event and close the dispatcher to create a new HTTP/2 session.
|
|
977
|
+
|
|
972
978
|
### Event: `'connectionError'`
|
|
973
979
|
|
|
974
980
|
Parameters:
|
package/lib/api/abort-signal.js
CHANGED
|
@@ -8,11 +8,14 @@ function abort (self) {
|
|
|
8
8
|
if (self.abort) {
|
|
9
9
|
self.abort(self[kSignal]?.reason)
|
|
10
10
|
} else {
|
|
11
|
-
self.
|
|
11
|
+
self.reason = self[kSignal]?.reason ?? new RequestAbortedError()
|
|
12
12
|
}
|
|
13
|
+
removeSignal(self)
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function addSignal (self, signal) {
|
|
17
|
+
self.reason = null
|
|
18
|
+
|
|
16
19
|
self[kSignal] = null
|
|
17
20
|
self[kListener] = null
|
|
18
21
|
|
package/lib/api/api-connect.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const assert = require('node:assert')
|
|
3
4
|
const { AsyncResource } = require('node:async_hooks')
|
|
4
|
-
const { InvalidArgumentError,
|
|
5
|
+
const { InvalidArgumentError, SocketError } = require('../core/errors')
|
|
5
6
|
const util = require('../core/util')
|
|
6
7
|
const { addSignal, removeSignal } = require('./abort-signal')
|
|
7
8
|
|
|
@@ -32,10 +33,13 @@ class ConnectHandler extends AsyncResource {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
onConnect (abort, context) {
|
|
35
|
-
if (
|
|
36
|
-
|
|
36
|
+
if (this.reason) {
|
|
37
|
+
abort(this.reason)
|
|
38
|
+
return
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
assert(this.callback)
|
|
42
|
+
|
|
39
43
|
this.abort = abort
|
|
40
44
|
this.context = context
|
|
41
45
|
}
|
package/lib/api/api-pipeline.js
CHANGED
|
@@ -147,12 +147,14 @@ class PipelineHandler extends AsyncResource {
|
|
|
147
147
|
onConnect (abort, context) {
|
|
148
148
|
const { ret, res } = this
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
throw new RequestAbortedError()
|
|
150
|
+
if (this.reason) {
|
|
151
|
+
abort(this.reason)
|
|
152
|
+
return
|
|
154
153
|
}
|
|
155
154
|
|
|
155
|
+
assert(!res, 'pipeline cannot be retried')
|
|
156
|
+
assert(!ret.destroyed)
|
|
157
|
+
|
|
156
158
|
this.abort = abort
|
|
157
159
|
this.context = context
|
|
158
160
|
}
|
package/lib/api/api-request.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const assert = require('node:assert')
|
|
3
4
|
const { Readable } = require('./readable')
|
|
4
|
-
const {
|
|
5
|
-
InvalidArgumentError,
|
|
6
|
-
RequestAbortedError
|
|
7
|
-
} = require('../core/errors')
|
|
5
|
+
const { InvalidArgumentError } = require('../core/errors')
|
|
8
6
|
const util = require('../core/util')
|
|
9
7
|
const { getResolveErrorBodyCallback } = require('./util')
|
|
10
8
|
const { AsyncResource } = require('node:async_hooks')
|
|
@@ -69,10 +67,13 @@ class RequestHandler extends AsyncResource {
|
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
onConnect (abort, context) {
|
|
72
|
-
if (
|
|
73
|
-
|
|
70
|
+
if (this.reason) {
|
|
71
|
+
abort(this.reason)
|
|
72
|
+
return
|
|
74
73
|
}
|
|
75
74
|
|
|
75
|
+
assert(this.callback)
|
|
76
|
+
|
|
76
77
|
this.abort = abort
|
|
77
78
|
this.context = context
|
|
78
79
|
}
|
package/lib/api/api-stream.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const assert = require('node:assert')
|
|
3
4
|
const { finished, PassThrough } = require('node:stream')
|
|
4
|
-
const {
|
|
5
|
-
InvalidArgumentError,
|
|
6
|
-
InvalidReturnValueError,
|
|
7
|
-
RequestAbortedError
|
|
8
|
-
} = require('../core/errors')
|
|
5
|
+
const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors')
|
|
9
6
|
const util = require('../core/util')
|
|
10
7
|
const { getResolveErrorBodyCallback } = require('./util')
|
|
11
8
|
const { AsyncResource } = require('node:async_hooks')
|
|
@@ -70,10 +67,13 @@ class StreamHandler extends AsyncResource {
|
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
onConnect (abort, context) {
|
|
73
|
-
if (
|
|
74
|
-
|
|
70
|
+
if (this.reason) {
|
|
71
|
+
abort(this.reason)
|
|
72
|
+
return
|
|
75
73
|
}
|
|
76
74
|
|
|
75
|
+
assert(this.callback)
|
|
76
|
+
|
|
77
77
|
this.abort = abort
|
|
78
78
|
this.context = context
|
|
79
79
|
}
|
package/lib/api/api-upgrade.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { InvalidArgumentError,
|
|
3
|
+
const { InvalidArgumentError, SocketError } = require('../core/errors')
|
|
4
4
|
const { AsyncResource } = require('node:async_hooks')
|
|
5
5
|
const util = require('../core/util')
|
|
6
6
|
const { addSignal, removeSignal } = require('./abort-signal')
|
|
@@ -34,10 +34,13 @@ class UpgradeHandler extends AsyncResource {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
onConnect (abort, context) {
|
|
37
|
-
if (
|
|
38
|
-
|
|
37
|
+
if (this.reason) {
|
|
38
|
+
abort(this.reason)
|
|
39
|
+
return
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
assert(this.callback)
|
|
43
|
+
|
|
41
44
|
this.abort = abort
|
|
42
45
|
this.context = null
|
|
43
46
|
}
|
package/lib/core/util.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
|
-
const { kDestroyed, kBodyUsed } = require('./symbols')
|
|
4
|
+
const { kDestroyed, kBodyUsed, kListeners } = require('./symbols')
|
|
5
5
|
const { IncomingMessage } = require('node:http')
|
|
6
6
|
const stream = require('node:stream')
|
|
7
7
|
const net = require('node:net')
|
|
@@ -22,13 +22,20 @@ function isStream (obj) {
|
|
|
22
22
|
|
|
23
23
|
// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
|
|
24
24
|
function isBlobLike (object) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
if (object === null) {
|
|
26
|
+
return false
|
|
27
|
+
} else if (object instanceof Blob) {
|
|
28
|
+
return true
|
|
29
|
+
} else if (typeof object !== 'object') {
|
|
30
|
+
return false
|
|
31
|
+
} else {
|
|
32
|
+
const sTag = object[Symbol.toStringTag]
|
|
33
|
+
|
|
34
|
+
return (sTag === 'Blob' || sTag === 'File') && (
|
|
35
|
+
('stream' in object && typeof object.stream === 'function') ||
|
|
36
|
+
('arrayBuffer' in object && typeof object.arrayBuffer === 'function')
|
|
37
|
+
)
|
|
38
|
+
}
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
function buildURL (url, queryParams) {
|
|
@@ -534,6 +541,29 @@ function parseRangeHeader (range) {
|
|
|
534
541
|
: null
|
|
535
542
|
}
|
|
536
543
|
|
|
544
|
+
function addListener (obj, name, listener) {
|
|
545
|
+
const listeners = (obj[kListeners] ??= [])
|
|
546
|
+
listeners.push([name, listener])
|
|
547
|
+
obj.on(name, listener)
|
|
548
|
+
return obj
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function removeAllListeners (obj) {
|
|
552
|
+
for (const [name, listener] of obj[kListeners] ?? []) {
|
|
553
|
+
obj.removeListener(name, listener)
|
|
554
|
+
}
|
|
555
|
+
obj[kListeners] = null
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function errorRequest (client, request, err) {
|
|
559
|
+
try {
|
|
560
|
+
request.onError(err)
|
|
561
|
+
assert(request.aborted)
|
|
562
|
+
} catch (err) {
|
|
563
|
+
client.emit('error', err)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
537
567
|
const kEnumerableProperty = Object.create(null)
|
|
538
568
|
kEnumerableProperty.enumerable = true
|
|
539
569
|
|
|
@@ -556,6 +586,9 @@ module.exports = {
|
|
|
556
586
|
isDestroyed,
|
|
557
587
|
headerNameToString,
|
|
558
588
|
bufferToLowerCasedHeaderName,
|
|
589
|
+
addListener,
|
|
590
|
+
removeAllListeners,
|
|
591
|
+
errorRequest,
|
|
559
592
|
parseRawHeaders,
|
|
560
593
|
parseHeaders,
|
|
561
594
|
parseKeepAliveTimeout,
|
|
@@ -47,7 +47,6 @@ const {
|
|
|
47
47
|
kMaxRequests,
|
|
48
48
|
kCounter,
|
|
49
49
|
kMaxResponseSize,
|
|
50
|
-
kListeners,
|
|
51
50
|
kOnError,
|
|
52
51
|
kResume,
|
|
53
52
|
kHTTPContext
|
|
@@ -56,23 +55,11 @@ const {
|
|
|
56
55
|
const constants = require('../llhttp/constants.js')
|
|
57
56
|
const EMPTY_BUF = Buffer.alloc(0)
|
|
58
57
|
const FastBuffer = Buffer[Symbol.species]
|
|
58
|
+
const addListener = util.addListener
|
|
59
|
+
const removeAllListeners = util.removeAllListeners
|
|
59
60
|
|
|
60
61
|
let extractBody
|
|
61
62
|
|
|
62
|
-
function addListener (obj, name, listener) {
|
|
63
|
-
const listeners = (obj[kListeners] ??= [])
|
|
64
|
-
listeners.push([name, listener])
|
|
65
|
-
obj.on(name, listener)
|
|
66
|
-
return obj
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function removeAllListeners (obj) {
|
|
70
|
-
for (const [name, listener] of obj[kListeners] ?? []) {
|
|
71
|
-
obj.removeListener(name, listener)
|
|
72
|
-
}
|
|
73
|
-
obj[kListeners] = null
|
|
74
|
-
}
|
|
75
|
-
|
|
76
63
|
async function lazyllhttp () {
|
|
77
64
|
const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined
|
|
78
65
|
|
|
@@ -719,14 +706,14 @@ async function connectH1 (client, socket) {
|
|
|
719
706
|
const requests = client[kQueue].splice(client[kRunningIdx])
|
|
720
707
|
for (let i = 0; i < requests.length; i++) {
|
|
721
708
|
const request = requests[i]
|
|
722
|
-
errorRequest(client, request, err)
|
|
709
|
+
util.errorRequest(client, request, err)
|
|
723
710
|
}
|
|
724
711
|
} else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') {
|
|
725
712
|
// Fail head of pipeline.
|
|
726
713
|
const request = client[kQueue][client[kRunningIdx]]
|
|
727
714
|
client[kQueue][client[kRunningIdx]++] = null
|
|
728
715
|
|
|
729
|
-
errorRequest(client, request, err)
|
|
716
|
+
util.errorRequest(client, request, err)
|
|
730
717
|
}
|
|
731
718
|
|
|
732
719
|
client[kPendingIdx] = client[kRunningIdx]
|
|
@@ -831,15 +818,6 @@ function resumeH1 (client) {
|
|
|
831
818
|
}
|
|
832
819
|
}
|
|
833
820
|
|
|
834
|
-
function errorRequest (client, request, err) {
|
|
835
|
-
try {
|
|
836
|
-
request.onError(err)
|
|
837
|
-
assert(request.aborted)
|
|
838
|
-
} catch (err) {
|
|
839
|
-
client.emit('error', err)
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
821
|
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
|
|
844
822
|
function shouldSendContentLength (method) {
|
|
845
823
|
return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
|
|
@@ -906,7 +884,7 @@ function writeH1 (client, request) {
|
|
|
906
884
|
// A user agent may send a Content-Length header with 0 value, this should be allowed.
|
|
907
885
|
if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) {
|
|
908
886
|
if (client[kStrictContentLength]) {
|
|
909
|
-
errorRequest(client, request, new RequestContentLengthMismatchError())
|
|
887
|
+
util.errorRequest(client, request, new RequestContentLengthMismatchError())
|
|
910
888
|
return false
|
|
911
889
|
}
|
|
912
890
|
|
|
@@ -915,22 +893,24 @@ function writeH1 (client, request) {
|
|
|
915
893
|
|
|
916
894
|
const socket = client[kSocket]
|
|
917
895
|
|
|
918
|
-
|
|
919
|
-
request.
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}
|
|
896
|
+
const abort = (err) => {
|
|
897
|
+
if (request.aborted || request.completed) {
|
|
898
|
+
return
|
|
899
|
+
}
|
|
923
900
|
|
|
924
|
-
|
|
901
|
+
util.errorRequest(client, request, err || new RequestAbortedError())
|
|
925
902
|
|
|
926
|
-
|
|
927
|
-
|
|
903
|
+
util.destroy(body)
|
|
904
|
+
util.destroy(socket, new InformationalError('aborted'))
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
request.onConnect(abort)
|
|
928
909
|
} catch (err) {
|
|
929
|
-
errorRequest(client, request, err)
|
|
910
|
+
util.errorRequest(client, request, err)
|
|
930
911
|
}
|
|
931
912
|
|
|
932
913
|
if (request.aborted) {
|
|
933
|
-
util.destroy(body)
|
|
934
914
|
return false
|
|
935
915
|
}
|
|
936
916
|
|
|
@@ -998,35 +978,19 @@ function writeH1 (client, request) {
|
|
|
998
978
|
|
|
999
979
|
/* istanbul ignore else: assertion */
|
|
1000
980
|
if (!body || bodyLength === 0) {
|
|
1001
|
-
|
|
1002
|
-
socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
|
|
1003
|
-
} else {
|
|
1004
|
-
assert(contentLength === null, 'no body must not have content length')
|
|
1005
|
-
socket.write(`${header}\r\n`, 'latin1')
|
|
1006
|
-
}
|
|
1007
|
-
request.onRequestSent()
|
|
981
|
+
writeBuffer({ abort, body: null, client, request, socket, contentLength, header, expectsPayload })
|
|
1008
982
|
} else if (util.isBuffer(body)) {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
socket.cork()
|
|
1012
|
-
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
|
|
1013
|
-
socket.write(body)
|
|
1014
|
-
socket.uncork()
|
|
1015
|
-
request.onBodySent(body)
|
|
1016
|
-
request.onRequestSent()
|
|
1017
|
-
if (!expectsPayload) {
|
|
1018
|
-
socket[kReset] = true
|
|
1019
|
-
}
|
|
983
|
+
writeBuffer({ abort, body, client, request, socket, contentLength, header, expectsPayload })
|
|
1020
984
|
} else if (util.isBlobLike(body)) {
|
|
1021
985
|
if (typeof body.stream === 'function') {
|
|
1022
|
-
writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload })
|
|
986
|
+
writeIterable({ abort, body: body.stream(), client, request, socket, contentLength, header, expectsPayload })
|
|
1023
987
|
} else {
|
|
1024
|
-
writeBlob({ body, client, request, socket, contentLength, header, expectsPayload })
|
|
988
|
+
writeBlob({ abort, body, client, request, socket, contentLength, header, expectsPayload })
|
|
1025
989
|
}
|
|
1026
990
|
} else if (util.isStream(body)) {
|
|
1027
|
-
writeStream({ body, client, request, socket, contentLength, header, expectsPayload })
|
|
991
|
+
writeStream({ abort, body, client, request, socket, contentLength, header, expectsPayload })
|
|
1028
992
|
} else if (util.isIterable(body)) {
|
|
1029
|
-
writeIterable({ body, client, request, socket, contentLength, header, expectsPayload })
|
|
993
|
+
writeIterable({ abort, body, client, request, socket, contentLength, header, expectsPayload })
|
|
1030
994
|
} else {
|
|
1031
995
|
assert(false)
|
|
1032
996
|
}
|
|
@@ -1034,12 +998,12 @@ function writeH1 (client, request) {
|
|
|
1034
998
|
return true
|
|
1035
999
|
}
|
|
1036
1000
|
|
|
1037
|
-
function writeStream ({
|
|
1001
|
+
function writeStream ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
1038
1002
|
assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
|
|
1039
1003
|
|
|
1040
1004
|
let finished = false
|
|
1041
1005
|
|
|
1042
|
-
const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header })
|
|
1006
|
+
const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header })
|
|
1043
1007
|
|
|
1044
1008
|
const onData = function (chunk) {
|
|
1045
1009
|
if (finished) {
|
|
@@ -1137,7 +1101,37 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
|
|
|
1137
1101
|
}
|
|
1138
1102
|
}
|
|
1139
1103
|
|
|
1140
|
-
async function
|
|
1104
|
+
async function writeBuffer ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
1105
|
+
try {
|
|
1106
|
+
if (!body) {
|
|
1107
|
+
if (contentLength === 0) {
|
|
1108
|
+
socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
|
|
1109
|
+
} else {
|
|
1110
|
+
assert(contentLength === null, 'no body must not have content length')
|
|
1111
|
+
socket.write(`${header}\r\n`, 'latin1')
|
|
1112
|
+
}
|
|
1113
|
+
} else if (util.isBuffer(body)) {
|
|
1114
|
+
assert(contentLength === body.byteLength, 'buffer body must have content length')
|
|
1115
|
+
|
|
1116
|
+
socket.cork()
|
|
1117
|
+
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
|
|
1118
|
+
socket.write(body)
|
|
1119
|
+
socket.uncork()
|
|
1120
|
+
request.onBodySent(body)
|
|
1121
|
+
|
|
1122
|
+
if (!expectsPayload) {
|
|
1123
|
+
socket[kReset] = true
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
request.onRequestSent()
|
|
1127
|
+
|
|
1128
|
+
client[kResume]()
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
abort(err)
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function writeBlob ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
1141
1135
|
assert(contentLength === body.size, 'blob body must have content length')
|
|
1142
1136
|
|
|
1143
1137
|
try {
|
|
@@ -1161,11 +1155,11 @@ async function writeBlob ({ h2stream, body, client, request, socket, contentLeng
|
|
|
1161
1155
|
|
|
1162
1156
|
client[kResume]()
|
|
1163
1157
|
} catch (err) {
|
|
1164
|
-
|
|
1158
|
+
abort(err)
|
|
1165
1159
|
}
|
|
1166
1160
|
}
|
|
1167
1161
|
|
|
1168
|
-
async function writeIterable ({
|
|
1162
|
+
async function writeIterable ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
1169
1163
|
assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
|
|
1170
1164
|
|
|
1171
1165
|
let callback = null
|
|
@@ -1191,7 +1185,7 @@ async function writeIterable ({ h2stream, body, client, request, socket, content
|
|
|
1191
1185
|
.on('close', onDrain)
|
|
1192
1186
|
.on('drain', onDrain)
|
|
1193
1187
|
|
|
1194
|
-
const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header })
|
|
1188
|
+
const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header })
|
|
1195
1189
|
try {
|
|
1196
1190
|
// It's up to the user to somehow abort the async iterable.
|
|
1197
1191
|
for await (const chunk of body) {
|
|
@@ -1215,7 +1209,7 @@ async function writeIterable ({ h2stream, body, client, request, socket, content
|
|
|
1215
1209
|
}
|
|
1216
1210
|
|
|
1217
1211
|
class AsyncWriter {
|
|
1218
|
-
constructor ({ socket, request, contentLength, client, expectsPayload, header }) {
|
|
1212
|
+
constructor ({ abort, socket, request, contentLength, client, expectsPayload, header }) {
|
|
1219
1213
|
this.socket = socket
|
|
1220
1214
|
this.request = request
|
|
1221
1215
|
this.contentLength = contentLength
|
|
@@ -1223,6 +1217,7 @@ class AsyncWriter {
|
|
|
1223
1217
|
this.bytesWritten = 0
|
|
1224
1218
|
this.expectsPayload = expectsPayload
|
|
1225
1219
|
this.header = header
|
|
1220
|
+
this.abort = abort
|
|
1226
1221
|
|
|
1227
1222
|
socket[kWriting] = true
|
|
1228
1223
|
}
|
|
@@ -1338,13 +1333,13 @@ class AsyncWriter {
|
|
|
1338
1333
|
}
|
|
1339
1334
|
|
|
1340
1335
|
destroy (err) {
|
|
1341
|
-
const { socket, client } = this
|
|
1336
|
+
const { socket, client, abort } = this
|
|
1342
1337
|
|
|
1343
1338
|
socket[kWriting] = false
|
|
1344
1339
|
|
|
1345
1340
|
if (err) {
|
|
1346
1341
|
assert(client[kRunning] <= 1, 'pipeline should only contain this request')
|
|
1347
|
-
|
|
1342
|
+
abort(err)
|
|
1348
1343
|
}
|
|
1349
1344
|
}
|
|
1350
1345
|
}
|
|
@@ -22,7 +22,6 @@ const {
|
|
|
22
22
|
kSocket,
|
|
23
23
|
kStrictContentLength,
|
|
24
24
|
kOnError,
|
|
25
|
-
// HTTP2
|
|
26
25
|
kMaxConcurrentStreams,
|
|
27
26
|
kHTTP2Session,
|
|
28
27
|
kResume
|
|
@@ -55,14 +54,20 @@ const {
|
|
|
55
54
|
} = http2
|
|
56
55
|
|
|
57
56
|
function parseH2Headers (headers) {
|
|
58
|
-
// set-cookie is always an array. Duplicates are added to the array.
|
|
59
|
-
// For duplicate cookie headers, the values are joined together with '; '.
|
|
60
|
-
headers = Object.entries(headers).flat(2)
|
|
61
|
-
|
|
62
57
|
const result = []
|
|
63
58
|
|
|
64
|
-
for (const
|
|
65
|
-
|
|
59
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
60
|
+
// h2 may concat the header value by array
|
|
61
|
+
// e.g. Set-Cookie
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
for (const subvalue of value) {
|
|
64
|
+
// we need to provide each header value of header name
|
|
65
|
+
// because the headers handler expect name-value pair
|
|
66
|
+
result.push(Buffer.from(name), Buffer.from(subvalue))
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
result.push(Buffer.from(name), Buffer.from(value))
|
|
70
|
+
}
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
return result
|
|
@@ -86,16 +91,18 @@ async function connectH2 (client, socket) {
|
|
|
86
91
|
session[kOpenStreams] = 0
|
|
87
92
|
session[kClient] = client
|
|
88
93
|
session[kSocket] = socket
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
|
|
95
|
+
util.addListener(session, 'error', onHttp2SessionError)
|
|
96
|
+
util.addListener(session, 'frameError', onHttp2FrameError)
|
|
97
|
+
util.addListener(session, 'end', onHttp2SessionEnd)
|
|
98
|
+
util.addListener(session, 'goaway', onHTTP2GoAway)
|
|
99
|
+
util.addListener(session, 'close', function () {
|
|
94
100
|
const { [kClient]: client } = this
|
|
95
101
|
|
|
96
|
-
const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
|
|
102
|
+
const err = this[kSocket][kError] || new SocketError('closed', util.getSocketInfo(this))
|
|
97
103
|
|
|
98
104
|
client[kSocket] = null
|
|
105
|
+
client[kHTTP2Session] = null
|
|
99
106
|
|
|
100
107
|
assert(client[kPending] === 0)
|
|
101
108
|
|
|
@@ -103,7 +110,7 @@ async function connectH2 (client, socket) {
|
|
|
103
110
|
const requests = client[kQueue].splice(client[kRunningIdx])
|
|
104
111
|
for (let i = 0; i < requests.length; i++) {
|
|
105
112
|
const request = requests[i]
|
|
106
|
-
errorRequest(client, request, err)
|
|
113
|
+
util.errorRequest(client, request, err)
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
client[kPendingIdx] = client[kRunningIdx]
|
|
@@ -114,19 +121,21 @@ async function connectH2 (client, socket) {
|
|
|
114
121
|
|
|
115
122
|
client[kResume]()
|
|
116
123
|
})
|
|
124
|
+
|
|
117
125
|
session.unref()
|
|
118
126
|
|
|
119
127
|
client[kHTTP2Session] = session
|
|
120
128
|
socket[kHTTP2Session] = session
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
util.addListener(socket, 'error', function (err) {
|
|
123
131
|
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
124
132
|
|
|
125
133
|
this[kError] = err
|
|
126
134
|
|
|
127
135
|
this[kClient][kOnError](err)
|
|
128
136
|
})
|
|
129
|
-
|
|
137
|
+
|
|
138
|
+
util.addListener(socket, 'end', function () {
|
|
130
139
|
util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
|
|
131
140
|
})
|
|
132
141
|
|
|
@@ -166,67 +175,42 @@ function onHttp2SessionError (err) {
|
|
|
166
175
|
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
167
176
|
|
|
168
177
|
this[kSocket][kError] = err
|
|
169
|
-
|
|
170
178
|
this[kClient][kOnError](err)
|
|
171
179
|
}
|
|
172
180
|
|
|
173
181
|
function onHttp2FrameError (type, code, id) {
|
|
174
|
-
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
|
|
175
|
-
|
|
176
182
|
if (id === 0) {
|
|
183
|
+
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
|
|
177
184
|
this[kSocket][kError] = err
|
|
178
185
|
this[kClient][kOnError](err)
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
188
|
|
|
182
189
|
function onHttp2SessionEnd () {
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
const err = new SocketError('other side closed', util.getSocketInfo(this[kSocket]))
|
|
191
|
+
this.destroy(err)
|
|
192
|
+
util.destroy(this[kSocket], err)
|
|
185
193
|
}
|
|
186
194
|
|
|
195
|
+
/**
|
|
196
|
+
* This is the root cause of #3011
|
|
197
|
+
* We need to handle GOAWAY frames properly, and trigger the session close
|
|
198
|
+
* along with the socket right away
|
|
199
|
+
* Find a way to trigger the close cycle from here on.
|
|
200
|
+
*/
|
|
187
201
|
function onHTTP2GoAway (code) {
|
|
188
|
-
const client = this[kClient]
|
|
189
202
|
const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`)
|
|
190
|
-
client[kSocket] = null
|
|
191
|
-
client[kHTTP2Session] = null
|
|
192
203
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
for (let i = 0; i < requests.length; i++) {
|
|
199
|
-
const request = requests[i]
|
|
200
|
-
errorRequest(this, request, err)
|
|
201
|
-
}
|
|
202
|
-
} else if (client[kRunning] > 0) {
|
|
203
|
-
// Fail head of pipeline.
|
|
204
|
-
const request = client[kQueue][client[kRunningIdx]]
|
|
205
|
-
client[kQueue][client[kRunningIdx]++] = null
|
|
206
|
-
|
|
207
|
-
errorRequest(client, request, err)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
client[kPendingIdx] = client[kRunningIdx]
|
|
211
|
-
|
|
212
|
-
assert(client[kRunning] === 0)
|
|
213
|
-
|
|
214
|
-
client.emit('disconnect',
|
|
215
|
-
client[kUrl],
|
|
216
|
-
[client],
|
|
217
|
-
err
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
client[kResume]()
|
|
221
|
-
}
|
|
204
|
+
// We need to trigger the close cycle right away
|
|
205
|
+
// We need to destroy the session and the socket
|
|
206
|
+
// Requests should be failed with the error after the current one is handled
|
|
207
|
+
this[kSocket][kError] = err
|
|
208
|
+
this[kClient][kOnError](err)
|
|
222
209
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
} catch (err) {
|
|
228
|
-
client.emit('error', err)
|
|
229
|
-
}
|
|
210
|
+
this.unref()
|
|
211
|
+
// We send the GOAWAY frame response as no error
|
|
212
|
+
this.destroy()
|
|
213
|
+
util.destroy(this[kSocket], err)
|
|
230
214
|
}
|
|
231
215
|
|
|
232
216
|
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
|
|
@@ -239,7 +223,7 @@ function writeH2 (client, request) {
|
|
|
239
223
|
const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
|
|
240
224
|
|
|
241
225
|
if (upgrade) {
|
|
242
|
-
errorRequest(client, request, new Error('Upgrade not supported for H2'))
|
|
226
|
+
util.errorRequest(client, request, new Error('Upgrade not supported for H2'))
|
|
243
227
|
return false
|
|
244
228
|
}
|
|
245
229
|
|
|
@@ -292,10 +276,10 @@ function writeH2 (client, request) {
|
|
|
292
276
|
}
|
|
293
277
|
}
|
|
294
278
|
|
|
295
|
-
errorRequest(client, request, err)
|
|
279
|
+
util.errorRequest(client, request, err)
|
|
296
280
|
})
|
|
297
281
|
} catch (err) {
|
|
298
|
-
errorRequest(client, request, err)
|
|
282
|
+
util.errorRequest(client, request, err)
|
|
299
283
|
}
|
|
300
284
|
|
|
301
285
|
if (method === 'CONNECT') {
|
|
@@ -370,7 +354,7 @@ function writeH2 (client, request) {
|
|
|
370
354
|
// A user agent may send a Content-Length header with 0 value, this should be allowed.
|
|
371
355
|
if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) {
|
|
372
356
|
if (client[kStrictContentLength]) {
|
|
373
|
-
errorRequest(client, request, new RequestContentLengthMismatchError())
|
|
357
|
+
util.errorRequest(client, request, new RequestContentLengthMismatchError())
|
|
374
358
|
return false
|
|
375
359
|
}
|
|
376
360
|
|
|
@@ -412,7 +396,7 @@ function writeH2 (client, request) {
|
|
|
412
396
|
// as there's no value to keep it open.
|
|
413
397
|
if (request.aborted || request.completed) {
|
|
414
398
|
const err = new RequestAbortedError()
|
|
415
|
-
errorRequest(client, request, err)
|
|
399
|
+
util.errorRequest(client, request, err)
|
|
416
400
|
util.destroy(stream, err)
|
|
417
401
|
return
|
|
418
402
|
}
|
|
@@ -446,13 +430,12 @@ function writeH2 (client, request) {
|
|
|
446
430
|
}
|
|
447
431
|
|
|
448
432
|
const err = new InformationalError('HTTP/2: stream half-closed (remote)')
|
|
449
|
-
errorRequest(client, request, err)
|
|
433
|
+
util.errorRequest(client, request, err)
|
|
450
434
|
util.destroy(stream, err)
|
|
451
435
|
})
|
|
452
436
|
|
|
453
437
|
stream.once('close', () => {
|
|
454
438
|
session[kOpenStreams] -= 1
|
|
455
|
-
// TODO(HTTP/2): unref only if current streams count is 0
|
|
456
439
|
if (session[kOpenStreams] === 0) {
|
|
457
440
|
session.unref()
|
|
458
441
|
}
|
|
@@ -461,13 +444,14 @@ function writeH2 (client, request) {
|
|
|
461
444
|
stream.once('error', function (err) {
|
|
462
445
|
if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) {
|
|
463
446
|
session[kOpenStreams] -= 1
|
|
447
|
+
util.errorRequest(client, request, err)
|
|
464
448
|
util.destroy(stream, err)
|
|
465
449
|
}
|
|
466
450
|
})
|
|
467
451
|
|
|
468
452
|
stream.once('frameError', (type, code) => {
|
|
469
453
|
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
|
|
470
|
-
errorRequest(client, request, err)
|
|
454
|
+
util.errorRequest(client, request, err)
|
|
471
455
|
|
|
472
456
|
if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) {
|
|
473
457
|
session[kOpenStreams] -= 1
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -338,7 +338,7 @@ class Client extends DispatcherBase {
|
|
|
338
338
|
const requests = this[kQueue].splice(this[kPendingIdx])
|
|
339
339
|
for (let i = 0; i < requests.length; i++) {
|
|
340
340
|
const request = requests[i]
|
|
341
|
-
errorRequest(this, request, err)
|
|
341
|
+
util.errorRequest(this, request, err)
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
const callback = () => {
|
|
@@ -378,7 +378,7 @@ function onError (client, err) {
|
|
|
378
378
|
const requests = client[kQueue].splice(client[kRunningIdx])
|
|
379
379
|
for (let i = 0; i < requests.length; i++) {
|
|
380
380
|
const request = requests[i]
|
|
381
|
-
errorRequest(client, request, err)
|
|
381
|
+
util.errorRequest(client, request, err)
|
|
382
382
|
}
|
|
383
383
|
assert(client[kSize] === 0)
|
|
384
384
|
}
|
|
@@ -502,7 +502,7 @@ async function connect (client) {
|
|
|
502
502
|
assert(client[kRunning] === 0)
|
|
503
503
|
while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) {
|
|
504
504
|
const request = client[kQueue][client[kPendingIdx]++]
|
|
505
|
-
errorRequest(client, request, err)
|
|
505
|
+
util.errorRequest(client, request, err)
|
|
506
506
|
}
|
|
507
507
|
} else {
|
|
508
508
|
onError(client, err)
|
|
@@ -581,7 +581,10 @@ function _resume (client, sync) {
|
|
|
581
581
|
}
|
|
582
582
|
|
|
583
583
|
client[kServerName] = request.servername
|
|
584
|
-
client[kHTTPContext]?.destroy(new InformationalError('servername changed'))
|
|
584
|
+
client[kHTTPContext]?.destroy(new InformationalError('servername changed'), () => {
|
|
585
|
+
client[kHTTPContext] = null
|
|
586
|
+
resume(client)
|
|
587
|
+
})
|
|
585
588
|
}
|
|
586
589
|
|
|
587
590
|
if (client[kConnecting]) {
|
|
@@ -609,13 +612,4 @@ function _resume (client, sync) {
|
|
|
609
612
|
}
|
|
610
613
|
}
|
|
611
614
|
|
|
612
|
-
function errorRequest (client, request, err) {
|
|
613
|
-
try {
|
|
614
|
-
request.onError(err)
|
|
615
|
-
assert(request.aborted)
|
|
616
|
-
} catch (err) {
|
|
617
|
-
client.emit('error', err)
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
615
|
module.exports = Client
|
|
@@ -64,19 +64,19 @@ class ProxyAgent extends DispatcherBase {
|
|
|
64
64
|
this[kAgent] = new Agent({
|
|
65
65
|
...opts,
|
|
66
66
|
connect: async (opts, callback) => {
|
|
67
|
-
let
|
|
67
|
+
let requestedPath = opts.host
|
|
68
68
|
if (!opts.port) {
|
|
69
|
-
|
|
69
|
+
requestedPath += `:${defaultProtocolPort(opts.protocol)}`
|
|
70
70
|
}
|
|
71
71
|
try {
|
|
72
72
|
const { socket, statusCode } = await this[kClient].connect({
|
|
73
73
|
origin,
|
|
74
74
|
port,
|
|
75
|
-
path:
|
|
75
|
+
path: requestedPath,
|
|
76
76
|
signal: opts.signal,
|
|
77
77
|
headers: {
|
|
78
78
|
...this[kProxyHeaders],
|
|
79
|
-
host:
|
|
79
|
+
host: opts.host
|
|
80
80
|
},
|
|
81
81
|
servername: this[kProxyTls]?.servername || proxyHostname
|
|
82
82
|
})
|
|
@@ -108,16 +108,18 @@ class ProxyAgent extends DispatcherBase {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
dispatch (opts, handler) {
|
|
111
|
-
const { host } = new URL(opts.origin)
|
|
112
111
|
const headers = buildHeaders(opts.headers)
|
|
113
112
|
throwIfProxyAuthIsSent(headers)
|
|
113
|
+
|
|
114
|
+
if (headers && !('host' in headers) && !('Host' in headers)) {
|
|
115
|
+
const { host } = new URL(opts.origin)
|
|
116
|
+
headers.host = host
|
|
117
|
+
}
|
|
118
|
+
|
|
114
119
|
return this[kAgent].dispatch(
|
|
115
120
|
{
|
|
116
121
|
...opts,
|
|
117
|
-
headers
|
|
118
|
-
...headers,
|
|
119
|
-
host
|
|
120
|
-
}
|
|
122
|
+
headers
|
|
121
123
|
},
|
|
122
124
|
handler
|
|
123
125
|
)
|
|
@@ -90,7 +90,7 @@ class MockInterceptor {
|
|
|
90
90
|
this[kContentLength] = false
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
createMockScopeDispatchData (statusCode, data, responseOptions
|
|
93
|
+
createMockScopeDispatchData ({ statusCode, data, responseOptions }) {
|
|
94
94
|
const responseData = getResponseData(data)
|
|
95
95
|
const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {}
|
|
96
96
|
const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers }
|
|
@@ -99,14 +99,11 @@ class MockInterceptor {
|
|
|
99
99
|
return { statusCode, data, headers, trailers }
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
validateReplyParameters (
|
|
103
|
-
if (typeof statusCode === 'undefined') {
|
|
102
|
+
validateReplyParameters (replyParameters) {
|
|
103
|
+
if (typeof replyParameters.statusCode === 'undefined') {
|
|
104
104
|
throw new InvalidArgumentError('statusCode must be defined')
|
|
105
105
|
}
|
|
106
|
-
if (typeof
|
|
107
|
-
throw new InvalidArgumentError('data must be defined')
|
|
108
|
-
}
|
|
109
|
-
if (typeof responseOptions !== 'object' || responseOptions === null) {
|
|
106
|
+
if (typeof replyParameters.responseOptions !== 'object' || replyParameters.responseOptions === null) {
|
|
110
107
|
throw new InvalidArgumentError('responseOptions must be an object')
|
|
111
108
|
}
|
|
112
109
|
}
|
|
@@ -114,28 +111,28 @@ class MockInterceptor {
|
|
|
114
111
|
/**
|
|
115
112
|
* Mock an undici request with a defined reply.
|
|
116
113
|
*/
|
|
117
|
-
reply (
|
|
114
|
+
reply (replyOptionsCallbackOrStatusCode) {
|
|
118
115
|
// Values of reply aren't available right now as they
|
|
119
116
|
// can only be available when the reply callback is invoked.
|
|
120
|
-
if (typeof
|
|
117
|
+
if (typeof replyOptionsCallbackOrStatusCode === 'function') {
|
|
121
118
|
// We'll first wrap the provided callback in another function,
|
|
122
119
|
// this function will properly resolve the data from the callback
|
|
123
120
|
// when invoked.
|
|
124
121
|
const wrappedDefaultsCallback = (opts) => {
|
|
125
122
|
// Our reply options callback contains the parameter for statusCode, data and options.
|
|
126
|
-
const resolvedData =
|
|
123
|
+
const resolvedData = replyOptionsCallbackOrStatusCode(opts)
|
|
127
124
|
|
|
128
125
|
// Check if it is in the right format
|
|
129
|
-
if (typeof resolvedData !== 'object') {
|
|
126
|
+
if (typeof resolvedData !== 'object' || resolvedData === null) {
|
|
130
127
|
throw new InvalidArgumentError('reply options callback must return an object')
|
|
131
128
|
}
|
|
132
129
|
|
|
133
|
-
const {
|
|
134
|
-
this.validateReplyParameters(
|
|
130
|
+
const replyParameters = { data: '', responseOptions: {}, ...resolvedData }
|
|
131
|
+
this.validateReplyParameters(replyParameters)
|
|
135
132
|
// Since the values can be obtained immediately we return them
|
|
136
133
|
// from this higher order function that will be resolved later.
|
|
137
134
|
return {
|
|
138
|
-
...this.createMockScopeDispatchData(
|
|
135
|
+
...this.createMockScopeDispatchData(replyParameters)
|
|
139
136
|
}
|
|
140
137
|
}
|
|
141
138
|
|
|
@@ -148,11 +145,15 @@ class MockInterceptor {
|
|
|
148
145
|
// we should have 1-3 parameters. So we spread the arguments of
|
|
149
146
|
// this function to obtain the parameters, since replyData will always
|
|
150
147
|
// just be the statusCode.
|
|
151
|
-
const
|
|
152
|
-
|
|
148
|
+
const replyParameters = {
|
|
149
|
+
statusCode: replyOptionsCallbackOrStatusCode,
|
|
150
|
+
data: arguments[1] === undefined ? '' : arguments[1],
|
|
151
|
+
responseOptions: arguments[2] === undefined ? {} : arguments[2]
|
|
152
|
+
}
|
|
153
|
+
this.validateReplyParameters(replyParameters)
|
|
153
154
|
|
|
154
155
|
// Send in-already provided data like usual
|
|
155
|
-
const dispatchData = this.createMockScopeDispatchData(
|
|
156
|
+
const dispatchData = this.createMockScopeDispatchData(replyParameters)
|
|
156
157
|
const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData)
|
|
157
158
|
return new MockScope(newMockDispatch)
|
|
158
159
|
}
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -8,7 +8,7 @@ const {
|
|
|
8
8
|
kOrigin,
|
|
9
9
|
kGetNetConnect
|
|
10
10
|
} = require('./mock-symbols')
|
|
11
|
-
const { buildURL
|
|
11
|
+
const { buildURL } = require('../core/util')
|
|
12
12
|
const { STATUS_CODES } = require('node:http')
|
|
13
13
|
const {
|
|
14
14
|
types: {
|
|
@@ -285,10 +285,10 @@ function mockDispatch (opts, handler) {
|
|
|
285
285
|
const responseHeaders = generateKeyValues(headers)
|
|
286
286
|
const responseTrailers = generateKeyValues(trailers)
|
|
287
287
|
|
|
288
|
-
handler.
|
|
289
|
-
handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode))
|
|
290
|
-
handler.onData(Buffer.from(responseData))
|
|
291
|
-
handler.onComplete(responseTrailers)
|
|
288
|
+
handler.onConnect?.(err => handler.onError(err), null)
|
|
289
|
+
handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode))
|
|
290
|
+
handler.onData?.(Buffer.from(responseData))
|
|
291
|
+
handler.onComplete?.(responseTrailers)
|
|
292
292
|
deleteMockDispatch(mockDispatches, key)
|
|
293
293
|
}
|
|
294
294
|
|
|
@@ -14,8 +14,8 @@ const badPorts = [
|
|
|
14
14
|
'87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
|
|
15
15
|
'139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
|
|
16
16
|
'540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
|
|
17
|
-
'2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '
|
|
18
|
-
'10080'
|
|
17
|
+
'2049', '3659', '4045', '4190', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6679',
|
|
18
|
+
'6697', '10080'
|
|
19
19
|
]
|
|
20
20
|
|
|
21
21
|
const badPortsSet = new Set(badPorts)
|
|
@@ -4,7 +4,7 @@ const { Writable } = require('node:stream')
|
|
|
4
4
|
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
|
|
5
5
|
const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
|
|
6
6
|
const { channels } = require('../../core/diagnostics')
|
|
7
|
-
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = require('./util')
|
|
7
|
+
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode } = require('./util')
|
|
8
8
|
const { WebsocketFrameSend } = require('./frame')
|
|
9
9
|
|
|
10
10
|
// This code was influenced by ws released under the MIT license.
|
|
@@ -314,8 +314,7 @@ class ByteParser extends Writable {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
try {
|
|
317
|
-
|
|
318
|
-
reason = new TextDecoder('utf-8', { fatal: true }).decode(reason)
|
|
317
|
+
reason = utf8Decode(reason)
|
|
319
318
|
} catch {
|
|
320
319
|
return null
|
|
321
320
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = require('./symbols')
|
|
4
4
|
const { states, opcodes } = require('./constants')
|
|
5
5
|
const { MessageEvent, ErrorEvent } = require('./events')
|
|
6
|
+
const { isUtf8 } = require('node:buffer')
|
|
6
7
|
|
|
7
8
|
/* globals Blob */
|
|
8
9
|
|
|
@@ -87,7 +88,7 @@ function websocketMessageReceived (ws, type, data) {
|
|
|
87
88
|
// -> type indicates that the data is Text
|
|
88
89
|
// a new DOMString containing data
|
|
89
90
|
try {
|
|
90
|
-
dataForEvent =
|
|
91
|
+
dataForEvent = utf8Decode(data)
|
|
91
92
|
} catch {
|
|
92
93
|
failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.')
|
|
93
94
|
return
|
|
@@ -200,6 +201,33 @@ function failWebsocketConnection (ws, reason) {
|
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
// https://nodejs.org/api/intl.html#detecting-internationalization-support
|
|
205
|
+
const hasIntl = typeof process.versions.icu === 'string'
|
|
206
|
+
const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Converts a Buffer to utf-8, even on platforms without icu.
|
|
210
|
+
* @param {Buffer} buffer
|
|
211
|
+
*/
|
|
212
|
+
function utf8Decode (buffer) {
|
|
213
|
+
if (hasIntl) {
|
|
214
|
+
return fatalDecoder.decode(buffer)
|
|
215
|
+
} else {
|
|
216
|
+
if (!isUtf8?.(buffer)) {
|
|
217
|
+
// TODO: remove once node 18 or < node v18.14.0 is dropped
|
|
218
|
+
if (!isUtf8) {
|
|
219
|
+
process.emitWarning('ICU is not supported and no fallback exists. Please upgrade to at least Node v18.14.0.', {
|
|
220
|
+
code: 'UNDICI-WS-NO-ICU'
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
throw new TypeError('Invalid utf-8 received.')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return buffer.toString('utf-8')
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
203
231
|
module.exports = {
|
|
204
232
|
isConnecting,
|
|
205
233
|
isEstablished,
|
|
@@ -209,5 +237,6 @@ module.exports = {
|
|
|
209
237
|
isValidSubprotocol,
|
|
210
238
|
isValidStatusCode,
|
|
211
239
|
failWebsocketConnection,
|
|
212
|
-
websocketMessageReceived
|
|
240
|
+
websocketMessageReceived,
|
|
241
|
+
utf8Decode
|
|
213
242
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.12.0",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -68,20 +68,25 @@
|
|
|
68
68
|
"lint": "standard | snazzy",
|
|
69
69
|
"lint:fix": "standard --fix | snazzy",
|
|
70
70
|
"test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript",
|
|
71
|
-
"test:javascript": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:jest",
|
|
72
|
-
"test:javascript:withoutintl": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch:nobuild && npm run test:cookies && npm run test:eventsource:nobuild && npm run test:wpt:withoutintl && npm run test:node-test",
|
|
71
|
+
"test:javascript": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:cache && npm run test:interceptors && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:jest",
|
|
72
|
+
"test:javascript:withoutintl": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch:nobuild && npm run test:cache && npm run test:interceptors && npm run test:cookies && npm run test:eventsource:nobuild && npm run test:wpt:withoutintl && npm run test:node-test",
|
|
73
|
+
"test:busboy": "borp -p \"test/busboy/*.js\"",
|
|
74
|
+
"test:cache": "borp -p \"test/cache/*.js\"",
|
|
73
75
|
"test:cookies": "borp -p \"test/cookie/*.js\"",
|
|
74
|
-
"test:node-fetch": "borp -p \"test/node-fetch/**/*.js\"",
|
|
75
76
|
"test:eventsource": "npm run build:node && npm run test:eventsource:nobuild",
|
|
76
77
|
"test:eventsource:nobuild": "borp --expose-gc -p \"test/eventsource/*.js\"",
|
|
78
|
+
"test:fuzzing": "node test/fuzzing/fuzzing.test.js",
|
|
77
79
|
"test:fetch": "npm run build:node && npm run test:fetch:nobuild",
|
|
78
|
-
"test:fetch:nobuild": "borp --expose-gc -p \"test/fetch/*.js\" &&
|
|
80
|
+
"test:fetch:nobuild": "borp --expose-gc -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy",
|
|
81
|
+
"test:interceptors": "borp -p \"test/interceptors/*.js\"",
|
|
79
82
|
"test:jest": "cross-env NODE_V8_COVERAGE= jest",
|
|
80
83
|
"test:unit": "borp --expose-gc -p \"test/*.js\"",
|
|
84
|
+
"test:node-fetch": "borp -p \"test/node-fetch/**/*.js\"",
|
|
81
85
|
"test:node-test": "borp -p \"test/node-test/**/*.js\"",
|
|
82
86
|
"test:tdd": "borp --expose-gc -p \"test/*.js\"",
|
|
83
87
|
"test:tdd:node-test": "borp -p \"test/node-test/**/*.js\" -w",
|
|
84
88
|
"test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts",
|
|
89
|
+
"test:webidl": "borp -p \"test/webidl/*.js\"",
|
|
85
90
|
"test:websocket": "borp -p \"test/websocket/*.js\"",
|
|
86
91
|
"test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
|
|
87
92
|
"test:wpt:withoutintl": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
|
|
@@ -92,8 +97,7 @@
|
|
|
92
97
|
"coverage:report:ci": "c8 report",
|
|
93
98
|
"bench": "echo \"Error: Benchmarks have been moved to '/benchmarks'\" && exit 1",
|
|
94
99
|
"serve:website": "echo \"Error: Documentation has been moved to '/docs'\" && exit 1",
|
|
95
|
-
"prepare": "husky install && node ./scripts/platform-shell.js"
|
|
96
|
-
"fuzz": "jsfuzz test/fuzzing/fuzz.js corpus"
|
|
100
|
+
"prepare": "husky install && node ./scripts/platform-shell.js"
|
|
97
101
|
},
|
|
98
102
|
"devDependencies": {
|
|
99
103
|
"@matteo.collina/tspl": "^0.1.1",
|
|
@@ -104,13 +108,13 @@
|
|
|
104
108
|
"c8": "^9.1.0",
|
|
105
109
|
"cross-env": "^7.0.3",
|
|
106
110
|
"dns-packet": "^5.4.0",
|
|
111
|
+
"fast-check": "^3.17.1",
|
|
107
112
|
"form-data": "^4.0.0",
|
|
108
113
|
"formdata-node": "^6.0.3",
|
|
109
114
|
"https-pem": "^3.0.0",
|
|
110
115
|
"husky": "^9.0.7",
|
|
111
116
|
"jest": "^29.0.2",
|
|
112
117
|
"jsdom": "^24.0.0",
|
|
113
|
-
"jsfuzz": "^1.0.15",
|
|
114
118
|
"node-forge": "^1.3.1",
|
|
115
119
|
"pre-commit": "^1.2.2",
|
|
116
120
|
"proxy": "^2.1.1",
|
package/types/dispatcher.d.ts
CHANGED
|
@@ -225,7 +225,7 @@ declare namespace Dispatcher {
|
|
|
225
225
|
/** Invoked when response is received, before headers have been read. **/
|
|
226
226
|
onResponseStarted?(): void;
|
|
227
227
|
/** Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. */
|
|
228
|
-
onHeaders?(statusCode: number, headers: Buffer[]
|
|
228
|
+
onHeaders?(statusCode: number, headers: Buffer[], resume: () => void, statusText: string): boolean;
|
|
229
229
|
/** Invoked when response payload data is received. */
|
|
230
230
|
onData?(chunk: Buffer): boolean;
|
|
231
231
|
/** Invoked when response payload and trailers have been received and the request has completed. */
|
package/types/fetch.d.ts
CHANGED
|
@@ -163,7 +163,7 @@ export declare class Request extends BodyMixin {
|
|
|
163
163
|
readonly method: string
|
|
164
164
|
readonly mode: RequestMode
|
|
165
165
|
readonly redirect: RequestRedirect
|
|
166
|
-
readonly referrerPolicy:
|
|
166
|
+
readonly referrerPolicy: ReferrerPolicy
|
|
167
167
|
readonly url: string
|
|
168
168
|
|
|
169
169
|
readonly keepalive: boolean
|