undici 6.6.2 → 6.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -27
- package/docs/{api → docs/api}/DiagnosticsChannel.md +2 -2
- package/docs/{api → docs/api}/Dispatcher.md +39 -3
- package/docs/docs/api/Fetch.md +57 -0
- package/docs/{api → docs/api}/ProxyAgent.md +5 -1
- package/docs/docs/api/RetryAgent.md +45 -0
- package/docs/{api → docs/api}/RetryHandler.md +1 -1
- package/docs/{api → docs/api}/api-lifecycle.md +33 -4
- package/docs/{best-practices → docs/best-practices}/proxy.md +6 -6
- package/index-fetch.js +9 -8
- package/index.js +31 -25
- package/lib/api/api-request.js +1 -1
- package/lib/api/readable.js +12 -9
- package/lib/api/util.js +8 -6
- package/lib/core/request.js +72 -135
- package/lib/core/symbols.js +6 -5
- package/lib/core/tree.js +46 -26
- package/lib/core/util.js +41 -20
- package/lib/{agent.js → dispatcher/agent.js} +4 -4
- package/lib/{balanced-pool.js → dispatcher/balanced-pool.js} +3 -3
- package/lib/dispatcher/client-h1.js +1352 -0
- package/lib/dispatcher/client-h2.js +639 -0
- package/lib/dispatcher/client.js +611 -0
- package/lib/{dispatcher-base.js → dispatcher/dispatcher-base.js} +2 -2
- package/lib/{pool-base.js → dispatcher/pool-base.js} +3 -3
- package/lib/{pool-stats.js → dispatcher/pool-stats.js} +1 -1
- package/lib/{pool.js → dispatcher/pool.js} +4 -4
- package/lib/{proxy-agent.js → dispatcher/proxy-agent.js} +29 -35
- package/lib/dispatcher/retry-agent.js +35 -0
- package/lib/global.js +1 -1
- package/lib/handler/{RetryHandler.js → retry-handler.js} +2 -2
- package/lib/interceptor/{redirectInterceptor.js → redirect-interceptor.js} +1 -1
- package/lib/mock/mock-agent.js +2 -2
- package/lib/mock/mock-client.js +1 -1
- package/lib/mock/mock-interceptor.js +2 -2
- package/lib/mock/mock-pool.js +1 -1
- package/lib/mock/mock-utils.js +6 -4
- package/lib/{cache → web/cache}/cache.js +2 -4
- package/lib/{cache → web/cache}/cachestorage.js +1 -1
- package/lib/web/cache/symbols.js +5 -0
- package/lib/{cache → web/cache}/util.js +5 -9
- package/lib/{cookies → web/cookies}/parse.js +1 -1
- package/lib/{cookies → web/cookies}/util.js +76 -60
- package/lib/{eventsource → web/eventsource}/eventsource.js +2 -6
- package/lib/{fetch → web/fetch}/body.js +56 -175
- package/lib/{fetch/dataURL.js → web/fetch/data-url.js} +5 -2
- package/lib/{compat → web/fetch}/dispatcher-weakref.js +1 -1
- package/lib/{fetch → web/fetch}/file.js +7 -8
- package/lib/web/fetch/formdata-parser.js +488 -0
- package/lib/{fetch → web/fetch}/formdata.js +7 -68
- package/lib/{fetch → web/fetch}/headers.js +99 -71
- package/lib/{fetch → web/fetch}/index.js +33 -25
- package/lib/{fetch → web/fetch}/request.js +15 -7
- package/lib/{fetch → web/fetch}/response.js +3 -3
- package/lib/{fetch → web/fetch}/symbols.js +2 -1
- package/lib/{fetch → web/fetch}/util.js +171 -48
- package/lib/{fetch → web/fetch}/webidl.js +46 -16
- package/lib/{fileapi → web/fileapi}/filereader.js +1 -1
- package/lib/{fileapi → web/fileapi}/util.js +1 -1
- package/lib/{websocket → web/websocket}/connection.js +20 -10
- package/lib/{websocket → web/websocket}/constants.js +7 -0
- package/lib/{websocket → web/websocket}/events.js +1 -1
- package/lib/{websocket → web/websocket}/frame.js +1 -0
- package/lib/{websocket → web/websocket}/receiver.js +9 -16
- package/lib/{websocket → web/websocket}/util.js +37 -23
- package/lib/{websocket → web/websocket}/websocket.js +21 -9
- package/package.json +26 -54
- package/types/dispatcher.d.ts +1 -1
- package/types/fetch.d.ts +20 -21
- package/types/index.d.ts +2 -1
- package/types/retry-agent.d.ts +11 -0
- package/types/webidl.d.ts +6 -1
- package/docs/api/Fetch.md +0 -27
- package/docs/assets/lifecycle-diagram.png +0 -0
- package/lib/cache/symbols.js +0 -5
- package/lib/client.js +0 -2295
- package/lib/llhttp/llhttp.wasm +0 -0
- package/lib/llhttp/llhttp_simd.wasm +0 -0
- /package/docs/{api → docs/api}/Agent.md +0 -0
- /package/docs/{api → docs/api}/BalancedPool.md +0 -0
- /package/docs/{api → docs/api}/CacheStorage.md +0 -0
- /package/docs/{api → docs/api}/Client.md +0 -0
- /package/docs/{api → docs/api}/Connector.md +0 -0
- /package/docs/{api → docs/api}/ContentType.md +0 -0
- /package/docs/{api → docs/api}/Cookies.md +0 -0
- /package/docs/{api → docs/api}/Debug.md +0 -0
- /package/docs/{api → docs/api}/DispatchInterceptor.md +0 -0
- /package/docs/{api → docs/api}/Errors.md +0 -0
- /package/docs/{api → docs/api}/EventSource.md +0 -0
- /package/docs/{api → docs/api}/MockAgent.md +0 -0
- /package/docs/{api → docs/api}/MockClient.md +0 -0
- /package/docs/{api → docs/api}/MockErrors.md +0 -0
- /package/docs/{api → docs/api}/MockPool.md +0 -0
- /package/docs/{api → docs/api}/Pool.md +0 -0
- /package/docs/{api → docs/api}/PoolStats.md +0 -0
- /package/docs/{api → docs/api}/RedirectHandler.md +0 -0
- /package/docs/{api → docs/api}/Util.md +0 -0
- /package/docs/{api → docs/api}/WebSocket.md +0 -0
- /package/docs/{best-practices → docs/best-practices}/client-certificate.md +0 -0
- /package/docs/{best-practices → docs/best-practices}/mocking-request.md +0 -0
- /package/docs/{best-practices → docs/best-practices}/writing-tests.md +0 -0
- /package/lib/{dispatcher.js → dispatcher/dispatcher.js} +0 -0
- /package/lib/{node → dispatcher}/fixed-queue.js +0 -0
- /package/lib/handler/{DecoratorHandler.js → decorator-handler.js} +0 -0
- /package/lib/handler/{RedirectHandler.js → redirect-handler.js} +0 -0
- /package/lib/{timers.js → util/timers.js} +0 -0
- /package/lib/{cookies → web/cookies}/constants.js +0 -0
- /package/lib/{cookies → web/cookies}/index.js +0 -0
- /package/lib/{eventsource → web/eventsource}/eventsource-stream.js +0 -0
- /package/lib/{eventsource → web/eventsource}/util.js +0 -0
- /package/lib/{fetch → web/fetch}/LICENSE +0 -0
- /package/lib/{fetch → web/fetch}/constants.js +0 -0
- /package/lib/{fetch → web/fetch}/global.js +0 -0
- /package/lib/{fileapi → web/fileapi}/encoding.js +0 -0
- /package/lib/{fileapi → web/fileapi}/progressevent.js +0 -0
- /package/lib/{fileapi → web/fileapi}/symbols.js +0 -0
- /package/lib/{websocket → web/websocket}/symbols.js +0 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { pipeline } = require('node:stream')
|
|
5
|
+
const util = require('../core/util.js')
|
|
6
|
+
const {
|
|
7
|
+
RequestContentLengthMismatchError,
|
|
8
|
+
RequestAbortedError,
|
|
9
|
+
SocketError,
|
|
10
|
+
InformationalError
|
|
11
|
+
} = require('../core/errors.js')
|
|
12
|
+
const {
|
|
13
|
+
kUrl,
|
|
14
|
+
kReset,
|
|
15
|
+
kClient,
|
|
16
|
+
kRunning,
|
|
17
|
+
kPending,
|
|
18
|
+
kQueue,
|
|
19
|
+
kPendingIdx,
|
|
20
|
+
kRunningIdx,
|
|
21
|
+
kError,
|
|
22
|
+
kSocket,
|
|
23
|
+
kStrictContentLength,
|
|
24
|
+
kOnError,
|
|
25
|
+
// HTTP2
|
|
26
|
+
kMaxConcurrentStreams,
|
|
27
|
+
kHTTP2Session,
|
|
28
|
+
kResume
|
|
29
|
+
} = require('../core/symbols.js')
|
|
30
|
+
|
|
31
|
+
const kOpenStreams = Symbol('open streams')
|
|
32
|
+
|
|
33
|
+
// Experimental
|
|
34
|
+
let h2ExperimentalWarned = false
|
|
35
|
+
|
|
36
|
+
/** @type {import('http2')} */
|
|
37
|
+
let http2
|
|
38
|
+
try {
|
|
39
|
+
http2 = require('node:http2')
|
|
40
|
+
} catch {
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
http2 = { constants: {} }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
constants: {
|
|
47
|
+
HTTP2_HEADER_AUTHORITY,
|
|
48
|
+
HTTP2_HEADER_METHOD,
|
|
49
|
+
HTTP2_HEADER_PATH,
|
|
50
|
+
HTTP2_HEADER_SCHEME,
|
|
51
|
+
HTTP2_HEADER_CONTENT_LENGTH,
|
|
52
|
+
HTTP2_HEADER_EXPECT,
|
|
53
|
+
HTTP2_HEADER_STATUS
|
|
54
|
+
}
|
|
55
|
+
} = http2
|
|
56
|
+
|
|
57
|
+
async function connectH2 (client, socket) {
|
|
58
|
+
client[kSocket] = socket
|
|
59
|
+
|
|
60
|
+
if (!h2ExperimentalWarned) {
|
|
61
|
+
h2ExperimentalWarned = true
|
|
62
|
+
process.emitWarning('H2 support is experimental, expect them to change at any time.', {
|
|
63
|
+
code: 'UNDICI-H2'
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const session = http2.connect(client[kUrl], {
|
|
68
|
+
createConnection: () => socket,
|
|
69
|
+
peerMaxConcurrentStreams: client[kMaxConcurrentStreams]
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
session[kOpenStreams] = 0
|
|
73
|
+
session[kClient] = client
|
|
74
|
+
session[kSocket] = socket
|
|
75
|
+
session.on('error', onHttp2SessionError)
|
|
76
|
+
session.on('frameError', onHttp2FrameError)
|
|
77
|
+
session.on('end', onHttp2SessionEnd)
|
|
78
|
+
session.on('goaway', onHTTP2GoAway)
|
|
79
|
+
session.on('close', function () {
|
|
80
|
+
const { [kClient]: client } = this
|
|
81
|
+
|
|
82
|
+
const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
|
|
83
|
+
|
|
84
|
+
client[kSocket] = null
|
|
85
|
+
|
|
86
|
+
assert(client[kPending] === 0)
|
|
87
|
+
|
|
88
|
+
// Fail entire queue.
|
|
89
|
+
const requests = client[kQueue].splice(client[kRunningIdx])
|
|
90
|
+
for (let i = 0; i < requests.length; i++) {
|
|
91
|
+
const request = requests[i]
|
|
92
|
+
errorRequest(client, request, err)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
client[kPendingIdx] = client[kRunningIdx]
|
|
96
|
+
|
|
97
|
+
assert(client[kRunning] === 0)
|
|
98
|
+
|
|
99
|
+
client.emit('disconnect', client[kUrl], [client], err)
|
|
100
|
+
|
|
101
|
+
client[kResume]()
|
|
102
|
+
})
|
|
103
|
+
session.unref()
|
|
104
|
+
|
|
105
|
+
client[kHTTP2Session] = session
|
|
106
|
+
socket[kHTTP2Session] = session
|
|
107
|
+
|
|
108
|
+
socket.on('error', function (err) {
|
|
109
|
+
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
110
|
+
|
|
111
|
+
this[kError] = err
|
|
112
|
+
|
|
113
|
+
this[kClient][kOnError](err)
|
|
114
|
+
})
|
|
115
|
+
socket.on('end', function () {
|
|
116
|
+
util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
let closed = false
|
|
120
|
+
socket.on('close', () => {
|
|
121
|
+
closed = true
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
version: 'h2',
|
|
126
|
+
defaultPipelining: Infinity,
|
|
127
|
+
write (...args) {
|
|
128
|
+
// TODO (fix): return
|
|
129
|
+
writeH2(client, ...args)
|
|
130
|
+
},
|
|
131
|
+
resume () {
|
|
132
|
+
|
|
133
|
+
},
|
|
134
|
+
destroy (err, callback) {
|
|
135
|
+
session.destroy(err)
|
|
136
|
+
if (closed) {
|
|
137
|
+
queueMicrotask(callback)
|
|
138
|
+
} else {
|
|
139
|
+
socket.destroy(err).on('close', callback)
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
get destroyed () {
|
|
143
|
+
return socket.destroyed
|
|
144
|
+
},
|
|
145
|
+
busy () {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function onHttp2SessionError (err) {
|
|
152
|
+
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
153
|
+
|
|
154
|
+
this[kSocket][kError] = err
|
|
155
|
+
|
|
156
|
+
this[kClient][kOnError](err)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function onHttp2FrameError (type, code, id) {
|
|
160
|
+
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
|
|
161
|
+
|
|
162
|
+
if (id === 0) {
|
|
163
|
+
this[kSocket][kError] = err
|
|
164
|
+
this[kClient][kOnError](err)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onHttp2SessionEnd () {
|
|
169
|
+
this.destroy(new SocketError('other side closed'))
|
|
170
|
+
util.destroy(this[kSocket], new SocketError('other side closed'))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function onHTTP2GoAway (code) {
|
|
174
|
+
const client = this[kClient]
|
|
175
|
+
const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`)
|
|
176
|
+
client[kSocket] = null
|
|
177
|
+
client[kHTTP2Session] = null
|
|
178
|
+
|
|
179
|
+
if (client.destroyed) {
|
|
180
|
+
assert(this[kPending] === 0)
|
|
181
|
+
|
|
182
|
+
// Fail entire queue.
|
|
183
|
+
const requests = client[kQueue].splice(client[kRunningIdx])
|
|
184
|
+
for (let i = 0; i < requests.length; i++) {
|
|
185
|
+
const request = requests[i]
|
|
186
|
+
errorRequest(this, request, err)
|
|
187
|
+
}
|
|
188
|
+
} else if (client[kRunning] > 0) {
|
|
189
|
+
// Fail head of pipeline.
|
|
190
|
+
const request = client[kQueue][client[kRunningIdx]]
|
|
191
|
+
client[kQueue][client[kRunningIdx]++] = null
|
|
192
|
+
|
|
193
|
+
errorRequest(client, request, err)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
client[kPendingIdx] = client[kRunningIdx]
|
|
197
|
+
|
|
198
|
+
assert(client[kRunning] === 0)
|
|
199
|
+
|
|
200
|
+
client.emit('disconnect',
|
|
201
|
+
client[kUrl],
|
|
202
|
+
[client],
|
|
203
|
+
err
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
client[kResume]()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function errorRequest (client, request, err) {
|
|
210
|
+
try {
|
|
211
|
+
request.onError(err)
|
|
212
|
+
assert(request.aborted)
|
|
213
|
+
} catch (err) {
|
|
214
|
+
client.emit('error', err)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
|
|
219
|
+
function shouldSendContentLength (method) {
|
|
220
|
+
return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function writeH2 (client, request) {
|
|
224
|
+
const session = client[kHTTP2Session]
|
|
225
|
+
const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
|
|
226
|
+
|
|
227
|
+
if (upgrade) {
|
|
228
|
+
errorRequest(client, request, new Error('Upgrade not supported for H2'))
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (request.aborted) {
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const headers = {}
|
|
237
|
+
for (let n = 0; n < reqHeaders.length; n += 2) {
|
|
238
|
+
const key = reqHeaders[n + 0]
|
|
239
|
+
const val = reqHeaders[n + 1]
|
|
240
|
+
|
|
241
|
+
if (Array.isArray(val)) {
|
|
242
|
+
for (let i = 0; i < val.length; i++) {
|
|
243
|
+
if (headers[key]) {
|
|
244
|
+
headers[key] += `,${val[i]}`
|
|
245
|
+
} else {
|
|
246
|
+
headers[key] = val[i]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
headers[key] = val
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** @type {import('node:http2').ClientHttp2Stream} */
|
|
255
|
+
let stream
|
|
256
|
+
|
|
257
|
+
const { hostname, port } = client[kUrl]
|
|
258
|
+
|
|
259
|
+
headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
|
|
260
|
+
headers[HTTP2_HEADER_METHOD] = method
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// We are already connected, streams are pending.
|
|
264
|
+
// We can call on connect, and wait for abort
|
|
265
|
+
request.onConnect((err) => {
|
|
266
|
+
if (request.aborted || request.completed) {
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
err = err || new RequestAbortedError()
|
|
271
|
+
|
|
272
|
+
if (stream != null) {
|
|
273
|
+
util.destroy(stream, err)
|
|
274
|
+
|
|
275
|
+
session[kOpenStreams] -= 1
|
|
276
|
+
if (session[kOpenStreams] === 0) {
|
|
277
|
+
session.unref()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
errorRequest(client, request, err)
|
|
282
|
+
})
|
|
283
|
+
} catch (err) {
|
|
284
|
+
errorRequest(client, request, err)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (method === 'CONNECT') {
|
|
288
|
+
session.ref()
|
|
289
|
+
// We are already connected, streams are pending, first request
|
|
290
|
+
// will create a new stream. We trigger a request to create the stream and wait until
|
|
291
|
+
// `ready` event is triggered
|
|
292
|
+
// We disabled endStream to allow the user to write to the stream
|
|
293
|
+
stream = session.request(headers, { endStream: false, signal })
|
|
294
|
+
|
|
295
|
+
if (stream.id && !stream.pending) {
|
|
296
|
+
request.onUpgrade(null, null, stream)
|
|
297
|
+
++session[kOpenStreams]
|
|
298
|
+
} else {
|
|
299
|
+
stream.once('ready', () => {
|
|
300
|
+
request.onUpgrade(null, null, stream)
|
|
301
|
+
++session[kOpenStreams]
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
stream.once('close', () => {
|
|
306
|
+
session[kOpenStreams] -= 1
|
|
307
|
+
// TODO(HTTP/2): unref only if current streams count is 0
|
|
308
|
+
if (session[kOpenStreams] === 0) session.unref()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
return true
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// https://tools.ietf.org/html/rfc7540#section-8.3
|
|
315
|
+
// :path and :scheme headers must be omitted when sending CONNECT
|
|
316
|
+
|
|
317
|
+
headers[HTTP2_HEADER_PATH] = path
|
|
318
|
+
headers[HTTP2_HEADER_SCHEME] = 'https'
|
|
319
|
+
|
|
320
|
+
// https://tools.ietf.org/html/rfc7231#section-4.3.1
|
|
321
|
+
// https://tools.ietf.org/html/rfc7231#section-4.3.2
|
|
322
|
+
// https://tools.ietf.org/html/rfc7231#section-4.3.5
|
|
323
|
+
|
|
324
|
+
// Sending a payload body on a request that does not
|
|
325
|
+
// expect it can cause undefined behavior on some
|
|
326
|
+
// servers and corrupt connection state. Do not
|
|
327
|
+
// re-use the connection for further requests.
|
|
328
|
+
|
|
329
|
+
const expectsPayload = (
|
|
330
|
+
method === 'PUT' ||
|
|
331
|
+
method === 'POST' ||
|
|
332
|
+
method === 'PATCH'
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if (body && typeof body.read === 'function') {
|
|
336
|
+
// Try to read EOF in order to get length.
|
|
337
|
+
body.read(0)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let contentLength = util.bodyLength(body)
|
|
341
|
+
|
|
342
|
+
if (contentLength == null) {
|
|
343
|
+
contentLength = request.contentLength
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (contentLength === 0 || !expectsPayload) {
|
|
347
|
+
// https://tools.ietf.org/html/rfc7230#section-3.3.2
|
|
348
|
+
// A user agent SHOULD NOT send a Content-Length header field when
|
|
349
|
+
// the request message does not contain a payload body and the method
|
|
350
|
+
// semantics do not anticipate such a body.
|
|
351
|
+
|
|
352
|
+
contentLength = null
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// https://github.com/nodejs/undici/issues/2046
|
|
356
|
+
// A user agent may send a Content-Length header with 0 value, this should be allowed.
|
|
357
|
+
if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) {
|
|
358
|
+
if (client[kStrictContentLength]) {
|
|
359
|
+
errorRequest(client, request, new RequestContentLengthMismatchError())
|
|
360
|
+
return false
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
process.emitWarning(new RequestContentLengthMismatchError())
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (contentLength != null) {
|
|
367
|
+
assert(body, 'no body must not have content length')
|
|
368
|
+
headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
session.ref()
|
|
372
|
+
|
|
373
|
+
const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null
|
|
374
|
+
if (expectContinue) {
|
|
375
|
+
headers[HTTP2_HEADER_EXPECT] = '100-continue'
|
|
376
|
+
stream = session.request(headers, { endStream: shouldEndStream, signal })
|
|
377
|
+
|
|
378
|
+
stream.once('continue', writeBodyH2)
|
|
379
|
+
} else {
|
|
380
|
+
stream = session.request(headers, {
|
|
381
|
+
endStream: shouldEndStream,
|
|
382
|
+
signal
|
|
383
|
+
})
|
|
384
|
+
writeBodyH2()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Increment counter as we have new several streams open
|
|
388
|
+
++session[kOpenStreams]
|
|
389
|
+
|
|
390
|
+
stream.once('response', headers => {
|
|
391
|
+
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
392
|
+
request.onResponseStarted()
|
|
393
|
+
|
|
394
|
+
if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) {
|
|
395
|
+
stream.pause()
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
stream.once('end', () => {
|
|
400
|
+
// When state is null, it means we haven't consumed body and the stream still do not have
|
|
401
|
+
// a state.
|
|
402
|
+
// Present specially when using pipeline or stream
|
|
403
|
+
if (stream.state?.state == null || stream.state.state < 6) {
|
|
404
|
+
request.onComplete([])
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Stream is closed or half-closed-remote (6), decrement counter and cleanup
|
|
409
|
+
// It does not have sense to continue working with the stream as we do not
|
|
410
|
+
// have yet RST_STREAM support on client-side
|
|
411
|
+
session[kOpenStreams] -= 1
|
|
412
|
+
if (session[kOpenStreams] === 0) {
|
|
413
|
+
session.unref()
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const err = new InformationalError('HTTP/2: stream half-closed (remote)')
|
|
417
|
+
errorRequest(client, request, err)
|
|
418
|
+
util.destroy(stream, err)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
stream.on('data', (chunk) => {
|
|
422
|
+
if (request.onData(chunk) === false) {
|
|
423
|
+
stream.pause()
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
stream.once('close', () => {
|
|
428
|
+
session[kOpenStreams] -= 1
|
|
429
|
+
// TODO(HTTP/2): unref only if current streams count is 0
|
|
430
|
+
if (session[kOpenStreams] === 0) {
|
|
431
|
+
session.unref()
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
stream.once('error', function (err) {
|
|
436
|
+
if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) {
|
|
437
|
+
session[kOpenStreams] -= 1
|
|
438
|
+
util.destroy(stream, err)
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
stream.once('frameError', (type, code) => {
|
|
443
|
+
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
|
|
444
|
+
errorRequest(client, request, err)
|
|
445
|
+
|
|
446
|
+
if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) {
|
|
447
|
+
session[kOpenStreams] -= 1
|
|
448
|
+
util.destroy(stream, err)
|
|
449
|
+
}
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
// stream.on('aborted', () => {
|
|
453
|
+
// // TODO(HTTP/2): Support aborted
|
|
454
|
+
// })
|
|
455
|
+
|
|
456
|
+
// stream.on('timeout', () => {
|
|
457
|
+
// // TODO(HTTP/2): Support timeout
|
|
458
|
+
// })
|
|
459
|
+
|
|
460
|
+
// stream.on('push', headers => {
|
|
461
|
+
// // TODO(HTTP/2): Support push
|
|
462
|
+
// })
|
|
463
|
+
|
|
464
|
+
// stream.on('trailers', headers => {
|
|
465
|
+
// // TODO(HTTP/2): Support trailers
|
|
466
|
+
// })
|
|
467
|
+
|
|
468
|
+
return true
|
|
469
|
+
|
|
470
|
+
function writeBodyH2 () {
|
|
471
|
+
/* istanbul ignore else: assertion */
|
|
472
|
+
if (!body) {
|
|
473
|
+
request.onRequestSent()
|
|
474
|
+
} else if (util.isBuffer(body)) {
|
|
475
|
+
assert(contentLength === body.byteLength, 'buffer body must have content length')
|
|
476
|
+
stream.cork()
|
|
477
|
+
stream.write(body)
|
|
478
|
+
stream.uncork()
|
|
479
|
+
stream.end()
|
|
480
|
+
request.onBodySent(body)
|
|
481
|
+
request.onRequestSent()
|
|
482
|
+
} else if (util.isBlobLike(body)) {
|
|
483
|
+
if (typeof body.stream === 'function') {
|
|
484
|
+
writeIterable({
|
|
485
|
+
client,
|
|
486
|
+
request,
|
|
487
|
+
contentLength,
|
|
488
|
+
h2stream: stream,
|
|
489
|
+
expectsPayload,
|
|
490
|
+
body: body.stream(),
|
|
491
|
+
socket: client[kSocket],
|
|
492
|
+
header: ''
|
|
493
|
+
})
|
|
494
|
+
} else {
|
|
495
|
+
writeBlob({
|
|
496
|
+
body,
|
|
497
|
+
client,
|
|
498
|
+
request,
|
|
499
|
+
contentLength,
|
|
500
|
+
expectsPayload,
|
|
501
|
+
h2stream: stream,
|
|
502
|
+
header: '',
|
|
503
|
+
socket: client[kSocket]
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
} else if (util.isStream(body)) {
|
|
507
|
+
writeStream({
|
|
508
|
+
body,
|
|
509
|
+
client,
|
|
510
|
+
request,
|
|
511
|
+
contentLength,
|
|
512
|
+
expectsPayload,
|
|
513
|
+
socket: client[kSocket],
|
|
514
|
+
h2stream: stream,
|
|
515
|
+
header: ''
|
|
516
|
+
})
|
|
517
|
+
} else if (util.isIterable(body)) {
|
|
518
|
+
writeIterable({
|
|
519
|
+
body,
|
|
520
|
+
client,
|
|
521
|
+
request,
|
|
522
|
+
contentLength,
|
|
523
|
+
expectsPayload,
|
|
524
|
+
header: '',
|
|
525
|
+
h2stream: stream,
|
|
526
|
+
socket: client[kSocket]
|
|
527
|
+
})
|
|
528
|
+
} else {
|
|
529
|
+
assert(false)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
535
|
+
assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
|
|
536
|
+
|
|
537
|
+
// For HTTP/2, is enough to pipe the stream
|
|
538
|
+
const pipe = pipeline(
|
|
539
|
+
body,
|
|
540
|
+
h2stream,
|
|
541
|
+
(err) => {
|
|
542
|
+
if (err) {
|
|
543
|
+
util.destroy(body, err)
|
|
544
|
+
util.destroy(h2stream, err)
|
|
545
|
+
} else {
|
|
546
|
+
request.onRequestSent()
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
pipe.on('data', onPipeData)
|
|
552
|
+
pipe.once('end', () => {
|
|
553
|
+
pipe.removeListener('data', onPipeData)
|
|
554
|
+
util.destroy(pipe)
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
function onPipeData (chunk) {
|
|
558
|
+
request.onBodySent(chunk)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
563
|
+
assert(contentLength === body.size, 'blob body must have content length')
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
if (contentLength != null && contentLength !== body.size) {
|
|
567
|
+
throw new RequestContentLengthMismatchError()
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const buffer = Buffer.from(await body.arrayBuffer())
|
|
571
|
+
|
|
572
|
+
h2stream.cork()
|
|
573
|
+
h2stream.write(buffer)
|
|
574
|
+
h2stream.uncork()
|
|
575
|
+
|
|
576
|
+
request.onBodySent(buffer)
|
|
577
|
+
request.onRequestSent()
|
|
578
|
+
|
|
579
|
+
if (!expectsPayload) {
|
|
580
|
+
socket[kReset] = true
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
client[kResume]()
|
|
584
|
+
} catch (err) {
|
|
585
|
+
util.destroy(h2stream)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
590
|
+
assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
|
|
591
|
+
|
|
592
|
+
let callback = null
|
|
593
|
+
function onDrain () {
|
|
594
|
+
if (callback) {
|
|
595
|
+
const cb = callback
|
|
596
|
+
callback = null
|
|
597
|
+
cb()
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const waitForDrain = () => new Promise((resolve, reject) => {
|
|
602
|
+
assert(callback === null)
|
|
603
|
+
|
|
604
|
+
if (socket[kError]) {
|
|
605
|
+
reject(socket[kError])
|
|
606
|
+
} else {
|
|
607
|
+
callback = resolve
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
h2stream
|
|
612
|
+
.on('close', onDrain)
|
|
613
|
+
.on('drain', onDrain)
|
|
614
|
+
|
|
615
|
+
try {
|
|
616
|
+
// It's up to the user to somehow abort the async iterable.
|
|
617
|
+
for await (const chunk of body) {
|
|
618
|
+
if (socket[kError]) {
|
|
619
|
+
throw socket[kError]
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const res = h2stream.write(chunk)
|
|
623
|
+
request.onBodySent(chunk)
|
|
624
|
+
if (!res) {
|
|
625
|
+
await waitForDrain()
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} catch (err) {
|
|
629
|
+
h2stream.destroy(err)
|
|
630
|
+
} finally {
|
|
631
|
+
request.onRequestSent()
|
|
632
|
+
h2stream.end()
|
|
633
|
+
h2stream
|
|
634
|
+
.off('close', onDrain)
|
|
635
|
+
.off('drain', onDrain)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
module.exports = connectH2
|