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.
Files changed (117) hide show
  1. package/README.md +49 -27
  2. package/docs/{api → docs/api}/DiagnosticsChannel.md +2 -2
  3. package/docs/{api → docs/api}/Dispatcher.md +39 -3
  4. package/docs/docs/api/Fetch.md +57 -0
  5. package/docs/{api → docs/api}/ProxyAgent.md +5 -1
  6. package/docs/docs/api/RetryAgent.md +45 -0
  7. package/docs/{api → docs/api}/RetryHandler.md +1 -1
  8. package/docs/{api → docs/api}/api-lifecycle.md +33 -4
  9. package/docs/{best-practices → docs/best-practices}/proxy.md +6 -6
  10. package/index-fetch.js +9 -8
  11. package/index.js +31 -25
  12. package/lib/api/api-request.js +1 -1
  13. package/lib/api/readable.js +12 -9
  14. package/lib/api/util.js +8 -6
  15. package/lib/core/request.js +72 -135
  16. package/lib/core/symbols.js +6 -5
  17. package/lib/core/tree.js +46 -26
  18. package/lib/core/util.js +41 -20
  19. package/lib/{agent.js → dispatcher/agent.js} +4 -4
  20. package/lib/{balanced-pool.js → dispatcher/balanced-pool.js} +3 -3
  21. package/lib/dispatcher/client-h1.js +1352 -0
  22. package/lib/dispatcher/client-h2.js +639 -0
  23. package/lib/dispatcher/client.js +611 -0
  24. package/lib/{dispatcher-base.js → dispatcher/dispatcher-base.js} +2 -2
  25. package/lib/{pool-base.js → dispatcher/pool-base.js} +3 -3
  26. package/lib/{pool-stats.js → dispatcher/pool-stats.js} +1 -1
  27. package/lib/{pool.js → dispatcher/pool.js} +4 -4
  28. package/lib/{proxy-agent.js → dispatcher/proxy-agent.js} +29 -35
  29. package/lib/dispatcher/retry-agent.js +35 -0
  30. package/lib/global.js +1 -1
  31. package/lib/handler/{RetryHandler.js → retry-handler.js} +2 -2
  32. package/lib/interceptor/{redirectInterceptor.js → redirect-interceptor.js} +1 -1
  33. package/lib/mock/mock-agent.js +2 -2
  34. package/lib/mock/mock-client.js +1 -1
  35. package/lib/mock/mock-interceptor.js +2 -2
  36. package/lib/mock/mock-pool.js +1 -1
  37. package/lib/mock/mock-utils.js +6 -4
  38. package/lib/{cache → web/cache}/cache.js +2 -4
  39. package/lib/{cache → web/cache}/cachestorage.js +1 -1
  40. package/lib/web/cache/symbols.js +5 -0
  41. package/lib/{cache → web/cache}/util.js +5 -9
  42. package/lib/{cookies → web/cookies}/parse.js +1 -1
  43. package/lib/{cookies → web/cookies}/util.js +76 -60
  44. package/lib/{eventsource → web/eventsource}/eventsource.js +2 -6
  45. package/lib/{fetch → web/fetch}/body.js +56 -175
  46. package/lib/{fetch/dataURL.js → web/fetch/data-url.js} +5 -2
  47. package/lib/{compat → web/fetch}/dispatcher-weakref.js +1 -1
  48. package/lib/{fetch → web/fetch}/file.js +7 -8
  49. package/lib/web/fetch/formdata-parser.js +488 -0
  50. package/lib/{fetch → web/fetch}/formdata.js +7 -68
  51. package/lib/{fetch → web/fetch}/headers.js +99 -71
  52. package/lib/{fetch → web/fetch}/index.js +33 -25
  53. package/lib/{fetch → web/fetch}/request.js +15 -7
  54. package/lib/{fetch → web/fetch}/response.js +3 -3
  55. package/lib/{fetch → web/fetch}/symbols.js +2 -1
  56. package/lib/{fetch → web/fetch}/util.js +171 -48
  57. package/lib/{fetch → web/fetch}/webidl.js +46 -16
  58. package/lib/{fileapi → web/fileapi}/filereader.js +1 -1
  59. package/lib/{fileapi → web/fileapi}/util.js +1 -1
  60. package/lib/{websocket → web/websocket}/connection.js +20 -10
  61. package/lib/{websocket → web/websocket}/constants.js +7 -0
  62. package/lib/{websocket → web/websocket}/events.js +1 -1
  63. package/lib/{websocket → web/websocket}/frame.js +1 -0
  64. package/lib/{websocket → web/websocket}/receiver.js +9 -16
  65. package/lib/{websocket → web/websocket}/util.js +37 -23
  66. package/lib/{websocket → web/websocket}/websocket.js +21 -9
  67. package/package.json +26 -54
  68. package/types/dispatcher.d.ts +1 -1
  69. package/types/fetch.d.ts +20 -21
  70. package/types/index.d.ts +2 -1
  71. package/types/retry-agent.d.ts +11 -0
  72. package/types/webidl.d.ts +6 -1
  73. package/docs/api/Fetch.md +0 -27
  74. package/docs/assets/lifecycle-diagram.png +0 -0
  75. package/lib/cache/symbols.js +0 -5
  76. package/lib/client.js +0 -2295
  77. package/lib/llhttp/llhttp.wasm +0 -0
  78. package/lib/llhttp/llhttp_simd.wasm +0 -0
  79. /package/docs/{api → docs/api}/Agent.md +0 -0
  80. /package/docs/{api → docs/api}/BalancedPool.md +0 -0
  81. /package/docs/{api → docs/api}/CacheStorage.md +0 -0
  82. /package/docs/{api → docs/api}/Client.md +0 -0
  83. /package/docs/{api → docs/api}/Connector.md +0 -0
  84. /package/docs/{api → docs/api}/ContentType.md +0 -0
  85. /package/docs/{api → docs/api}/Cookies.md +0 -0
  86. /package/docs/{api → docs/api}/Debug.md +0 -0
  87. /package/docs/{api → docs/api}/DispatchInterceptor.md +0 -0
  88. /package/docs/{api → docs/api}/Errors.md +0 -0
  89. /package/docs/{api → docs/api}/EventSource.md +0 -0
  90. /package/docs/{api → docs/api}/MockAgent.md +0 -0
  91. /package/docs/{api → docs/api}/MockClient.md +0 -0
  92. /package/docs/{api → docs/api}/MockErrors.md +0 -0
  93. /package/docs/{api → docs/api}/MockPool.md +0 -0
  94. /package/docs/{api → docs/api}/Pool.md +0 -0
  95. /package/docs/{api → docs/api}/PoolStats.md +0 -0
  96. /package/docs/{api → docs/api}/RedirectHandler.md +0 -0
  97. /package/docs/{api → docs/api}/Util.md +0 -0
  98. /package/docs/{api → docs/api}/WebSocket.md +0 -0
  99. /package/docs/{best-practices → docs/best-practices}/client-certificate.md +0 -0
  100. /package/docs/{best-practices → docs/best-practices}/mocking-request.md +0 -0
  101. /package/docs/{best-practices → docs/best-practices}/writing-tests.md +0 -0
  102. /package/lib/{dispatcher.js → dispatcher/dispatcher.js} +0 -0
  103. /package/lib/{node → dispatcher}/fixed-queue.js +0 -0
  104. /package/lib/handler/{DecoratorHandler.js → decorator-handler.js} +0 -0
  105. /package/lib/handler/{RedirectHandler.js → redirect-handler.js} +0 -0
  106. /package/lib/{timers.js → util/timers.js} +0 -0
  107. /package/lib/{cookies → web/cookies}/constants.js +0 -0
  108. /package/lib/{cookies → web/cookies}/index.js +0 -0
  109. /package/lib/{eventsource → web/eventsource}/eventsource-stream.js +0 -0
  110. /package/lib/{eventsource → web/eventsource}/util.js +0 -0
  111. /package/lib/{fetch → web/fetch}/LICENSE +0 -0
  112. /package/lib/{fetch → web/fetch}/constants.js +0 -0
  113. /package/lib/{fetch → web/fetch}/global.js +0 -0
  114. /package/lib/{fileapi → web/fileapi}/encoding.js +0 -0
  115. /package/lib/{fileapi → web/fileapi}/progressevent.js +0 -0
  116. /package/lib/{fileapi → web/fileapi}/symbols.js +0 -0
  117. /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