undici 6.12.0 → 6.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/lib/api/readable.js +1 -3
- package/lib/dispatcher/client-h2.js +126 -80
- package/lib/handler/decorator-handler.js +17 -8
- package/lib/web/fetch/body.js +2 -2
- package/lib/web/fetch/formdata-parser.js +2 -40
- package/lib/web/fetch/index.js +7 -30
- package/lib/web/fetch/util.js +29 -30
- package/lib/web/websocket/util.js +11 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -248,6 +248,18 @@ const data = {
|
|
|
248
248
|
await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' })
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
+
[FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) besides text data and buffers can also utilize streams via [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects:
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
import { openAsBlob } from 'node:fs'
|
|
255
|
+
|
|
256
|
+
const file = await openAsBlob('./big.csv')
|
|
257
|
+
const body = new FormData()
|
|
258
|
+
body.set('file', file, 'big.csv')
|
|
259
|
+
|
|
260
|
+
await fetch('http://example.com', { method: 'POST', body })
|
|
261
|
+
```
|
|
262
|
+
|
|
251
263
|
#### `request.duplex`
|
|
252
264
|
|
|
253
265
|
- half
|
package/lib/api/readable.js
CHANGED
|
@@ -63,9 +63,7 @@ class BodyReadable extends Readable {
|
|
|
63
63
|
// tick as it is created, then a user who is waiting for a
|
|
64
64
|
// promise (i.e micro tick) for installing a 'error' listener will
|
|
65
65
|
// never get a chance and will always encounter an unhandled exception.
|
|
66
|
-
|
|
67
|
-
// - micro tick => queueMicrotask(fn)
|
|
68
|
-
queueMicrotask(() => {
|
|
66
|
+
setImmediate(() => {
|
|
69
67
|
callback(err)
|
|
70
68
|
})
|
|
71
69
|
}
|
|
@@ -98,28 +98,22 @@ async function connectH2 (client, socket) {
|
|
|
98
98
|
util.addListener(session, 'goaway', onHTTP2GoAway)
|
|
99
99
|
util.addListener(session, 'close', function () {
|
|
100
100
|
const { [kClient]: client } = this
|
|
101
|
+
const { [kSocket]: socket } = client
|
|
101
102
|
|
|
102
|
-
const err = this[kSocket][kError] || new SocketError('closed', util.getSocketInfo(
|
|
103
|
+
const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
|
|
103
104
|
|
|
104
|
-
client[kSocket] = null
|
|
105
105
|
client[kHTTP2Session] = null
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
if (client.destroyed) {
|
|
108
|
+
assert(client[kPending] === 0)
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
// Fail entire queue.
|
|
111
|
+
const requests = client[kQueue].splice(client[kRunningIdx])
|
|
112
|
+
for (let i = 0; i < requests.length; i++) {
|
|
113
|
+
const request = requests[i]
|
|
114
|
+
util.errorRequest(client, request, err)
|
|
115
|
+
}
|
|
114
116
|
}
|
|
115
|
-
|
|
116
|
-
client[kPendingIdx] = client[kRunningIdx]
|
|
117
|
-
|
|
118
|
-
assert(client[kRunning] === 0)
|
|
119
|
-
|
|
120
|
-
client.emit('disconnect', client[kUrl], [client], err)
|
|
121
|
-
|
|
122
|
-
client[kResume]()
|
|
123
117
|
})
|
|
124
118
|
|
|
125
119
|
session.unref()
|
|
@@ -139,6 +133,24 @@ async function connectH2 (client, socket) {
|
|
|
139
133
|
util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
|
|
140
134
|
})
|
|
141
135
|
|
|
136
|
+
util.addListener(socket, 'close', function () {
|
|
137
|
+
const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
|
|
138
|
+
|
|
139
|
+
client[kSocket] = null
|
|
140
|
+
|
|
141
|
+
if (this[kHTTP2Session] != null) {
|
|
142
|
+
this[kHTTP2Session].destroy(err)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
client[kPendingIdx] = client[kRunningIdx]
|
|
146
|
+
|
|
147
|
+
assert(client[kRunning] === 0)
|
|
148
|
+
|
|
149
|
+
client.emit('disconnect', client[kUrl], [client], err)
|
|
150
|
+
|
|
151
|
+
client[kResume]()
|
|
152
|
+
})
|
|
153
|
+
|
|
142
154
|
let closed = false
|
|
143
155
|
socket.on('close', () => {
|
|
144
156
|
closed = true
|
|
@@ -155,10 +167,10 @@ async function connectH2 (client, socket) {
|
|
|
155
167
|
|
|
156
168
|
},
|
|
157
169
|
destroy (err, callback) {
|
|
158
|
-
session.destroy(err)
|
|
159
170
|
if (closed) {
|
|
160
171
|
queueMicrotask(callback)
|
|
161
172
|
} else {
|
|
173
|
+
// Destroying the socket will trigger the session close
|
|
162
174
|
socket.destroy(err).on('close', callback)
|
|
163
175
|
}
|
|
164
176
|
},
|
|
@@ -257,27 +269,28 @@ function writeH2 (client, request) {
|
|
|
257
269
|
headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
|
|
258
270
|
headers[HTTP2_HEADER_METHOD] = method
|
|
259
271
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (request.aborted || request.completed) {
|
|
265
|
-
return
|
|
266
|
-
}
|
|
272
|
+
const abort = (err) => {
|
|
273
|
+
if (request.aborted || request.completed) {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
267
276
|
|
|
268
|
-
|
|
277
|
+
err = err || new RequestAbortedError()
|
|
269
278
|
|
|
270
|
-
|
|
271
|
-
util.destroy(stream, err)
|
|
279
|
+
util.errorRequest(client, request, err)
|
|
272
280
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
}
|
|
281
|
+
if (stream != null) {
|
|
282
|
+
util.destroy(stream, err)
|
|
283
|
+
}
|
|
278
284
|
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
// We do not destroy the socket as we can continue using the session
|
|
286
|
+
// the stream get's destroyed and the session remains to create new streams
|
|
287
|
+
util.destroy(body, err)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// We are already connected, streams are pending.
|
|
292
|
+
// We can call on connect, and wait for abort
|
|
293
|
+
request.onConnect(abort)
|
|
281
294
|
} catch (err) {
|
|
282
295
|
util.errorRequest(client, request, err)
|
|
283
296
|
}
|
|
@@ -302,7 +315,6 @@ function writeH2 (client, request) {
|
|
|
302
315
|
|
|
303
316
|
stream.once('close', () => {
|
|
304
317
|
session[kOpenStreams] -= 1
|
|
305
|
-
// TODO(HTTP/2): unref only if current streams count is 0
|
|
306
318
|
if (session[kOpenStreams] === 0) session.unref()
|
|
307
319
|
})
|
|
308
320
|
|
|
@@ -382,7 +394,7 @@ function writeH2 (client, request) {
|
|
|
382
394
|
writeBodyH2()
|
|
383
395
|
}
|
|
384
396
|
|
|
385
|
-
// Increment counter as we have new
|
|
397
|
+
// Increment counter as we have new streams open
|
|
386
398
|
++session[kOpenStreams]
|
|
387
399
|
|
|
388
400
|
stream.once('response', headers => {
|
|
@@ -394,7 +406,7 @@ function writeH2 (client, request) {
|
|
|
394
406
|
// the request remains in-flight and headers hasn't been received yet
|
|
395
407
|
// for those scenarios, best effort is to destroy the stream immediately
|
|
396
408
|
// as there's no value to keep it open.
|
|
397
|
-
if (request.aborted
|
|
409
|
+
if (request.aborted) {
|
|
398
410
|
const err = new RequestAbortedError()
|
|
399
411
|
util.errorRequest(client, request, err)
|
|
400
412
|
util.destroy(stream, err)
|
|
@@ -424,14 +436,11 @@ function writeH2 (client, request) {
|
|
|
424
436
|
// Stream is closed or half-closed-remote (6), decrement counter and cleanup
|
|
425
437
|
// It does not have sense to continue working with the stream as we do not
|
|
426
438
|
// have yet RST_STREAM support on client-side
|
|
427
|
-
session[kOpenStreams] -= 1
|
|
428
439
|
if (session[kOpenStreams] === 0) {
|
|
429
440
|
session.unref()
|
|
430
441
|
}
|
|
431
442
|
|
|
432
|
-
|
|
433
|
-
util.errorRequest(client, request, err)
|
|
434
|
-
util.destroy(stream, err)
|
|
443
|
+
abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
|
|
435
444
|
})
|
|
436
445
|
|
|
437
446
|
stream.once('close', () => {
|
|
@@ -442,21 +451,11 @@ function writeH2 (client, request) {
|
|
|
442
451
|
})
|
|
443
452
|
|
|
444
453
|
stream.once('error', function (err) {
|
|
445
|
-
|
|
446
|
-
session[kOpenStreams] -= 1
|
|
447
|
-
util.errorRequest(client, request, err)
|
|
448
|
-
util.destroy(stream, err)
|
|
449
|
-
}
|
|
454
|
+
abort(err)
|
|
450
455
|
})
|
|
451
456
|
|
|
452
457
|
stream.once('frameError', (type, code) => {
|
|
453
|
-
|
|
454
|
-
util.errorRequest(client, request, err)
|
|
455
|
-
|
|
456
|
-
if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) {
|
|
457
|
-
session[kOpenStreams] -= 1
|
|
458
|
-
util.destroy(stream, err)
|
|
459
|
-
}
|
|
458
|
+
abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
|
|
460
459
|
})
|
|
461
460
|
|
|
462
461
|
// stream.on('aborted', () => {
|
|
@@ -479,37 +478,49 @@ function writeH2 (client, request) {
|
|
|
479
478
|
|
|
480
479
|
function writeBodyH2 () {
|
|
481
480
|
/* istanbul ignore else: assertion */
|
|
482
|
-
if (!body) {
|
|
483
|
-
|
|
481
|
+
if (!body || contentLength === 0) {
|
|
482
|
+
writeBuffer({
|
|
483
|
+
abort,
|
|
484
|
+
client,
|
|
485
|
+
request,
|
|
486
|
+
contentLength,
|
|
487
|
+
expectsPayload,
|
|
488
|
+
h2stream: stream,
|
|
489
|
+
body: null,
|
|
490
|
+
socket: client[kSocket]
|
|
491
|
+
})
|
|
484
492
|
} else if (util.isBuffer(body)) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
493
|
+
writeBuffer({
|
|
494
|
+
abort,
|
|
495
|
+
client,
|
|
496
|
+
request,
|
|
497
|
+
contentLength,
|
|
498
|
+
body,
|
|
499
|
+
expectsPayload,
|
|
500
|
+
h2stream: stream,
|
|
501
|
+
socket: client[kSocket]
|
|
502
|
+
})
|
|
492
503
|
} else if (util.isBlobLike(body)) {
|
|
493
504
|
if (typeof body.stream === 'function') {
|
|
494
505
|
writeIterable({
|
|
506
|
+
abort,
|
|
495
507
|
client,
|
|
496
508
|
request,
|
|
497
509
|
contentLength,
|
|
498
|
-
h2stream: stream,
|
|
499
510
|
expectsPayload,
|
|
511
|
+
h2stream: stream,
|
|
500
512
|
body: body.stream(),
|
|
501
|
-
socket: client[kSocket]
|
|
502
|
-
header: ''
|
|
513
|
+
socket: client[kSocket]
|
|
503
514
|
})
|
|
504
515
|
} else {
|
|
505
516
|
writeBlob({
|
|
517
|
+
abort,
|
|
506
518
|
body,
|
|
507
519
|
client,
|
|
508
520
|
request,
|
|
509
521
|
contentLength,
|
|
510
522
|
expectsPayload,
|
|
511
523
|
h2stream: stream,
|
|
512
|
-
header: '',
|
|
513
524
|
socket: client[kSocket]
|
|
514
525
|
})
|
|
515
526
|
}
|
|
@@ -541,7 +552,30 @@ function writeH2 (client, request) {
|
|
|
541
552
|
}
|
|
542
553
|
}
|
|
543
554
|
|
|
544
|
-
function
|
|
555
|
+
function writeBuffer ({ abort, h2stream, body, client, request, socket, contentLength, expectsPayload }) {
|
|
556
|
+
try {
|
|
557
|
+
if (body != null && util.isBuffer(body)) {
|
|
558
|
+
assert(contentLength === body.byteLength, 'buffer body must have content length')
|
|
559
|
+
h2stream.cork()
|
|
560
|
+
h2stream.write(body)
|
|
561
|
+
h2stream.uncork()
|
|
562
|
+
h2stream.end()
|
|
563
|
+
|
|
564
|
+
request.onBodySent(body)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!expectsPayload) {
|
|
568
|
+
socket[kReset] = true
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
request.onRequestSent()
|
|
572
|
+
client[kResume]()
|
|
573
|
+
} catch (error) {
|
|
574
|
+
abort(error)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function writeStream ({ abort, socket, expectsPayload, h2stream, body, client, request, contentLength }) {
|
|
545
579
|
assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
|
|
546
580
|
|
|
547
581
|
// For HTTP/2, is enough to pipe the stream
|
|
@@ -550,26 +584,29 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
|
|
|
550
584
|
h2stream,
|
|
551
585
|
(err) => {
|
|
552
586
|
if (err) {
|
|
553
|
-
util.destroy(
|
|
554
|
-
|
|
587
|
+
util.destroy(pipe, err)
|
|
588
|
+
abort(err)
|
|
555
589
|
} else {
|
|
590
|
+
util.removeAllListeners(pipe)
|
|
556
591
|
request.onRequestSent()
|
|
592
|
+
|
|
593
|
+
if (!expectsPayload) {
|
|
594
|
+
socket[kReset] = true
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
client[kResume]()
|
|
557
598
|
}
|
|
558
599
|
}
|
|
559
600
|
)
|
|
560
601
|
|
|
561
|
-
|
|
562
|
-
pipe.once('end', () => {
|
|
563
|
-
pipe.removeListener('data', onPipeData)
|
|
564
|
-
util.destroy(pipe)
|
|
565
|
-
})
|
|
602
|
+
util.addListener(pipe, 'data', onPipeData)
|
|
566
603
|
|
|
567
604
|
function onPipeData (chunk) {
|
|
568
605
|
request.onBodySent(chunk)
|
|
569
606
|
}
|
|
570
607
|
}
|
|
571
608
|
|
|
572
|
-
async function writeBlob ({ h2stream, body, client, request, socket, contentLength,
|
|
609
|
+
async function writeBlob ({ abort, h2stream, body, client, request, socket, contentLength, expectsPayload }) {
|
|
573
610
|
assert(contentLength === body.size, 'blob body must have content length')
|
|
574
611
|
|
|
575
612
|
try {
|
|
@@ -582,6 +619,7 @@ async function writeBlob ({ h2stream, body, client, request, socket, contentLeng
|
|
|
582
619
|
h2stream.cork()
|
|
583
620
|
h2stream.write(buffer)
|
|
584
621
|
h2stream.uncork()
|
|
622
|
+
h2stream.end()
|
|
585
623
|
|
|
586
624
|
request.onBodySent(buffer)
|
|
587
625
|
request.onRequestSent()
|
|
@@ -592,11 +630,11 @@ async function writeBlob ({ h2stream, body, client, request, socket, contentLeng
|
|
|
592
630
|
|
|
593
631
|
client[kResume]()
|
|
594
632
|
} catch (err) {
|
|
595
|
-
|
|
633
|
+
abort(err)
|
|
596
634
|
}
|
|
597
635
|
}
|
|
598
636
|
|
|
599
|
-
async function writeIterable ({ h2stream, body, client, request, socket, contentLength,
|
|
637
|
+
async function writeIterable ({ abort, h2stream, body, client, request, socket, contentLength, expectsPayload }) {
|
|
600
638
|
assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
|
|
601
639
|
|
|
602
640
|
let callback = null
|
|
@@ -635,11 +673,19 @@ async function writeIterable ({ h2stream, body, client, request, socket, content
|
|
|
635
673
|
await waitForDrain()
|
|
636
674
|
}
|
|
637
675
|
}
|
|
676
|
+
|
|
677
|
+
h2stream.end()
|
|
678
|
+
|
|
679
|
+
request.onRequestSent()
|
|
680
|
+
|
|
681
|
+
if (!expectsPayload) {
|
|
682
|
+
socket[kReset] = true
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
client[kResume]()
|
|
638
686
|
} catch (err) {
|
|
639
|
-
|
|
687
|
+
abort(err)
|
|
640
688
|
} finally {
|
|
641
|
-
request.onRequestSent()
|
|
642
|
-
h2stream.end()
|
|
643
689
|
h2stream
|
|
644
690
|
.off('close', onDrain)
|
|
645
691
|
.off('drain', onDrain)
|
|
@@ -1,35 +1,44 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
module.exports = class DecoratorHandler {
|
|
4
|
+
#handler
|
|
5
|
+
|
|
4
6
|
constructor (handler) {
|
|
5
|
-
|
|
7
|
+
if (typeof handler !== 'object' || handler === null) {
|
|
8
|
+
throw new TypeError('handler must be an object')
|
|
9
|
+
}
|
|
10
|
+
this.#handler = handler
|
|
6
11
|
}
|
|
7
12
|
|
|
8
13
|
onConnect (...args) {
|
|
9
|
-
return this
|
|
14
|
+
return this.#handler.onConnect?.(...args)
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
onError (...args) {
|
|
13
|
-
return this
|
|
18
|
+
return this.#handler.onError?.(...args)
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
onUpgrade (...args) {
|
|
17
|
-
return this
|
|
22
|
+
return this.#handler.onUpgrade?.(...args)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onResponseStarted (...args) {
|
|
26
|
+
return this.#handler.onResponseStarted?.(...args)
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
onHeaders (...args) {
|
|
21
|
-
return this
|
|
30
|
+
return this.#handler.onHeaders?.(...args)
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
onData (...args) {
|
|
25
|
-
return this
|
|
34
|
+
return this.#handler.onData?.(...args)
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
onComplete (...args) {
|
|
29
|
-
return this
|
|
38
|
+
return this.#handler.onComplete?.(...args)
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
onBodySent (...args) {
|
|
33
|
-
return this
|
|
42
|
+
return this.#handler.onBodySent?.(...args)
|
|
34
43
|
}
|
|
35
44
|
}
|
package/lib/web/fetch/body.js
CHANGED
|
@@ -403,14 +403,14 @@ function mixinBody (prototype) {
|
|
|
403
403
|
async function consumeBody (object, convertBytesToJSValue, instance) {
|
|
404
404
|
webidl.brandCheck(object, instance)
|
|
405
405
|
|
|
406
|
-
throwIfAborted(object[kState])
|
|
407
|
-
|
|
408
406
|
// 1. If object is unusable, then return a promise rejected
|
|
409
407
|
// with a TypeError.
|
|
410
408
|
if (bodyUnusable(object[kState].body)) {
|
|
411
409
|
throw new TypeError('Body is unusable')
|
|
412
410
|
}
|
|
413
411
|
|
|
412
|
+
throwIfAborted(object[kState])
|
|
413
|
+
|
|
414
414
|
// 2. Let promise be a new promise.
|
|
415
415
|
const promise = createDeferredPromise()
|
|
416
416
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util')
|
|
4
4
|
const { utf8DecodeBytes } = require('./util')
|
|
5
5
|
const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
|
|
6
6
|
const { isFileLike, File: UndiciFile } = require('./file')
|
|
@@ -60,43 +60,6 @@ function validateBoundary (boundary) {
|
|
|
60
60
|
return true
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
* @see https://andreubotella.github.io/multipart-form-data/#escape-a-multipart-form-data-name
|
|
65
|
-
* @param {string} name
|
|
66
|
-
* @param {string} [encoding='utf-8']
|
|
67
|
-
* @param {boolean} [isFilename=false]
|
|
68
|
-
*/
|
|
69
|
-
function escapeFormDataName (name, encoding = 'utf-8', isFilename = false) {
|
|
70
|
-
// 1. If isFilename is true:
|
|
71
|
-
if (isFilename) {
|
|
72
|
-
// 1.1. Set name to the result of converting name into a scalar value string.
|
|
73
|
-
name = toUSVString(name)
|
|
74
|
-
} else {
|
|
75
|
-
// 2. Otherwise:
|
|
76
|
-
|
|
77
|
-
// 2.1. Assert: name is a scalar value string.
|
|
78
|
-
assert(isUSVString(name))
|
|
79
|
-
|
|
80
|
-
// 2.2. Replace every occurrence of U+000D (CR) not followed by U+000A (LF),
|
|
81
|
-
// and every occurrence of U+000A (LF) not preceded by U+000D (CR), in
|
|
82
|
-
// name, by a string consisting of U+000D (CR) and U+000A (LF).
|
|
83
|
-
name = name.replace(/\r\n?|\r?\n/g, '\r\n')
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 3. Let encoded be the result of encoding name with encoding.
|
|
87
|
-
assert(Buffer.isEncoding(encoding))
|
|
88
|
-
|
|
89
|
-
// 4. Replace every 0x0A (LF) bytes in encoded with the byte sequence `%0A`,
|
|
90
|
-
// 0x0D (CR) with `%0D` and 0x22 (") with `%22`.
|
|
91
|
-
name = name
|
|
92
|
-
.replace(/\n/g, '%0A')
|
|
93
|
-
.replace(/\r/g, '%0D')
|
|
94
|
-
.replace(/"/g, '%22')
|
|
95
|
-
|
|
96
|
-
// 5. Return encoded.
|
|
97
|
-
return Buffer.from(name, encoding) // encoded
|
|
98
|
-
}
|
|
99
|
-
|
|
100
63
|
/**
|
|
101
64
|
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-parser
|
|
102
65
|
* @param {Buffer} input
|
|
@@ -497,6 +460,5 @@ function bufferStartsWith (buffer, start, position) {
|
|
|
497
460
|
|
|
498
461
|
module.exports = {
|
|
499
462
|
multipartFormDataParser,
|
|
500
|
-
validateBoundary
|
|
501
|
-
escapeFormDataName
|
|
463
|
+
validateBoundary
|
|
502
464
|
}
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -58,7 +58,7 @@ const {
|
|
|
58
58
|
subresourceSet
|
|
59
59
|
} = require('./constants')
|
|
60
60
|
const EE = require('node:events')
|
|
61
|
-
const { Readable, pipeline } = require('node:stream')
|
|
61
|
+
const { Readable, pipeline, finished } = require('node:stream')
|
|
62
62
|
const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor, bufferToLowerCasedHeaderName } = require('../../core/util')
|
|
63
63
|
const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require('./data-url')
|
|
64
64
|
const { getGlobalDispatcher } = require('../../global')
|
|
@@ -1080,42 +1080,19 @@ function fetchFinale (fetchParams, response) {
|
|
|
1080
1080
|
if (internalResponse.body == null) {
|
|
1081
1081
|
processResponseEndOfBody()
|
|
1082
1082
|
} else {
|
|
1083
|
+
// mcollina: all the following steps of the specs are skipped.
|
|
1084
|
+
// The internal transform stream is not needed.
|
|
1085
|
+
// See https://github.com/nodejs/undici/pull/3093#issuecomment-2050198541
|
|
1086
|
+
|
|
1083
1087
|
// 1. Let transformStream be a new TransformStream.
|
|
1084
1088
|
// 2. Let identityTransformAlgorithm be an algorithm which, given chunk, enqueues chunk in transformStream.
|
|
1085
1089
|
// 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm and flushAlgorithm
|
|
1086
1090
|
// set to processResponseEndOfBody.
|
|
1087
|
-
const transformStream = new TransformStream({
|
|
1088
|
-
start () { },
|
|
1089
|
-
transform (chunk, controller) {
|
|
1090
|
-
controller.enqueue(chunk)
|
|
1091
|
-
},
|
|
1092
|
-
flush: processResponseEndOfBody
|
|
1093
|
-
})
|
|
1094
|
-
|
|
1095
1091
|
// 4. Set internalResponse’s body’s stream to the result of internalResponse’s body’s stream piped through transformStream.
|
|
1096
|
-
internalResponse.body.stream.pipeThrough(transformStream)
|
|
1097
|
-
|
|
1098
|
-
const byteStream = new ReadableStream({
|
|
1099
|
-
readableStream: transformStream.readable,
|
|
1100
|
-
async start () {
|
|
1101
|
-
this._bodyReader = this.readableStream.getReader()
|
|
1102
|
-
},
|
|
1103
|
-
async pull (controller) {
|
|
1104
|
-
while (controller.desiredSize >= 0) {
|
|
1105
|
-
const { done, value } = await this._bodyReader.read()
|
|
1106
1092
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
break
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
controller.enqueue(value)
|
|
1113
|
-
}
|
|
1114
|
-
},
|
|
1115
|
-
type: 'bytes'
|
|
1093
|
+
finished(internalResponse.body.stream, () => {
|
|
1094
|
+
processResponseEndOfBody()
|
|
1116
1095
|
})
|
|
1117
|
-
|
|
1118
|
-
internalResponse.body.stream = byteStream
|
|
1119
1096
|
}
|
|
1120
1097
|
}
|
|
1121
1098
|
|
package/lib/web/fetch/util.js
CHANGED
|
@@ -73,14 +73,13 @@ function responseLocationURL (response, requestFragment) {
|
|
|
73
73
|
* @returns {boolean}
|
|
74
74
|
*/
|
|
75
75
|
function isValidEncodedURL (url) {
|
|
76
|
-
for (
|
|
77
|
-
const code =
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if ((code >= 0x00 && code <= 0x1F) || code === 0x7F) {
|
|
76
|
+
for (let i = 0; i < url.length; ++i) {
|
|
77
|
+
const code = url.charCodeAt(i)
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
code > 0x7E || // Non-US-ASCII + DEL
|
|
81
|
+
code < 0x20 // Control characters NUL - US
|
|
82
|
+
) {
|
|
84
83
|
return false
|
|
85
84
|
}
|
|
86
85
|
}
|
|
@@ -160,24 +159,15 @@ const isValidHeaderName = isValidHTTPToken
|
|
|
160
159
|
function isValidHeaderValue (potentialValue) {
|
|
161
160
|
// - Has no leading or trailing HTTP tab or space bytes.
|
|
162
161
|
// - Contains no 0x00 (NUL) or HTTP newline bytes.
|
|
163
|
-
|
|
164
|
-
potentialValue
|
|
165
|
-
potentialValue
|
|
166
|
-
potentialValue.
|
|
167
|
-
potentialValue.
|
|
168
|
-
|
|
169
|
-
return false
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
potentialValue.includes('\0') ||
|
|
162
|
+
return (
|
|
163
|
+
potentialValue[0] === '\t' ||
|
|
164
|
+
potentialValue[0] === ' ' ||
|
|
165
|
+
potentialValue[potentialValue.length - 1] === '\t' ||
|
|
166
|
+
potentialValue[potentialValue.length - 1] === ' ' ||
|
|
167
|
+
potentialValue.includes('\n') ||
|
|
174
168
|
potentialValue.includes('\r') ||
|
|
175
|
-
potentialValue.includes('\
|
|
176
|
-
)
|
|
177
|
-
return false
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return true
|
|
169
|
+
potentialValue.includes('\0')
|
|
170
|
+
) === false
|
|
181
171
|
}
|
|
182
172
|
|
|
183
173
|
// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
|
|
@@ -1169,13 +1159,21 @@ function urlIsLocal (url) {
|
|
|
1169
1159
|
|
|
1170
1160
|
/**
|
|
1171
1161
|
* @param {string|URL} url
|
|
1162
|
+
* @returns {boolean}
|
|
1172
1163
|
*/
|
|
1173
1164
|
function urlHasHttpsScheme (url) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1165
|
+
return (
|
|
1166
|
+
(
|
|
1167
|
+
typeof url === 'string' &&
|
|
1168
|
+
url[5] === ':' &&
|
|
1169
|
+
url[0] === 'h' &&
|
|
1170
|
+
url[1] === 't' &&
|
|
1171
|
+
url[2] === 't' &&
|
|
1172
|
+
url[3] === 'p' &&
|
|
1173
|
+
url[4] === 's'
|
|
1174
|
+
) ||
|
|
1175
|
+
url.protocol === 'https:'
|
|
1176
|
+
)
|
|
1179
1177
|
}
|
|
1180
1178
|
|
|
1181
1179
|
/**
|
|
@@ -1567,6 +1565,7 @@ function utf8DecodeBytes (buffer) {
|
|
|
1567
1565
|
module.exports = {
|
|
1568
1566
|
isAborted,
|
|
1569
1567
|
isCancelled,
|
|
1568
|
+
isValidEncodedURL,
|
|
1570
1569
|
createDeferredPromise,
|
|
1571
1570
|
ReadableStreamFrom,
|
|
1572
1571
|
tryUpgradeRequestToAPotentiallyTrustworthyURL,
|
|
@@ -209,24 +209,21 @@ const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undef
|
|
|
209
209
|
* Converts a Buffer to utf-8, even on platforms without icu.
|
|
210
210
|
* @param {Buffer} buffer
|
|
211
211
|
*/
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (!isUtf8?.(buffer)) {
|
|
217
|
-
// TODO: remove once node 18 or < node v18.14.0 is dropped
|
|
218
|
-
if (!isUtf8) {
|
|
212
|
+
const utf8Decode = hasIntl
|
|
213
|
+
? fatalDecoder.decode.bind(fatalDecoder)
|
|
214
|
+
: !isUtf8
|
|
215
|
+
? function () { // TODO: remove once node 18 or < node v18.14.0 is dropped
|
|
219
216
|
process.emitWarning('ICU is not supported and no fallback exists. Please upgrade to at least Node v18.14.0.', {
|
|
220
217
|
code: 'UNDICI-WS-NO-ICU'
|
|
221
218
|
})
|
|
219
|
+
throw new TypeError('Invalid utf-8 received.')
|
|
220
|
+
}
|
|
221
|
+
: function (buffer) {
|
|
222
|
+
if (isUtf8(buffer)) {
|
|
223
|
+
return buffer.toString('utf-8')
|
|
224
|
+
}
|
|
225
|
+
throw new TypeError('Invalid utf-8 received.')
|
|
222
226
|
}
|
|
223
|
-
|
|
224
|
-
throw new TypeError('Invalid utf-8 received.')
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return buffer.toString('utf-8')
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
227
|
|
|
231
228
|
module.exports = {
|
|
232
229
|
isConnecting,
|