source: node_modules/undici/lib/fetch/body.js@ d24f17c

main
Last change on this file since d24f17c was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 18.9 KB
Line 
1'use strict'
2
3const Busboy = require('@fastify/busboy')
4const util = require('../core/util')
5const {
6 ReadableStreamFrom,
7 isBlobLike,
8 isReadableStreamLike,
9 readableStreamClose,
10 createDeferredPromise,
11 fullyReadBody
12} = require('./util')
13const { FormData } = require('./formdata')
14const { kState } = require('./symbols')
15const { webidl } = require('./webidl')
16const { DOMException, structuredClone } = require('./constants')
17const { Blob, File: NativeFile } = require('buffer')
18const { kBodyUsed } = require('../core/symbols')
19const assert = require('assert')
20const { isErrored } = require('../core/util')
21const { isUint8Array, isArrayBuffer } = require('util/types')
22const { File: UndiciFile } = require('./file')
23const { parseMIMEType, serializeAMimeType } = require('./dataURL')
24
25let ReadableStream = globalThis.ReadableStream
26
27/** @type {globalThis['File']} */
28const File = NativeFile ?? UndiciFile
29const textEncoder = new TextEncoder()
30const textDecoder = new TextDecoder()
31
32// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
33function extractBody (object, keepalive = false) {
34 if (!ReadableStream) {
35 ReadableStream = require('stream/web').ReadableStream
36 }
37
38 // 1. Let stream be null.
39 let stream = null
40
41 // 2. If object is a ReadableStream object, then set stream to object.
42 if (object instanceof ReadableStream) {
43 stream = object
44 } else if (isBlobLike(object)) {
45 // 3. Otherwise, if object is a Blob object, set stream to the
46 // result of running object’s get stream.
47 stream = object.stream()
48 } else {
49 // 4. Otherwise, set stream to a new ReadableStream object, and set
50 // up stream.
51 stream = new ReadableStream({
52 async pull (controller) {
53 controller.enqueue(
54 typeof source === 'string' ? textEncoder.encode(source) : source
55 )
56 queueMicrotask(() => readableStreamClose(controller))
57 },
58 start () {},
59 type: undefined
60 })
61 }
62
63 // 5. Assert: stream is a ReadableStream object.
64 assert(isReadableStreamLike(stream))
65
66 // 6. Let action be null.
67 let action = null
68
69 // 7. Let source be null.
70 let source = null
71
72 // 8. Let length be null.
73 let length = null
74
75 // 9. Let type be null.
76 let type = null
77
78 // 10. Switch on object:
79 if (typeof object === 'string') {
80 // Set source to the UTF-8 encoding of object.
81 // Note: setting source to a Uint8Array here breaks some mocking assumptions.
82 source = object
83
84 // Set type to `text/plain;charset=UTF-8`.
85 type = 'text/plain;charset=UTF-8'
86 } else if (object instanceof URLSearchParams) {
87 // URLSearchParams
88
89 // spec says to run application/x-www-form-urlencoded on body.list
90 // this is implemented in Node.js as apart of an URLSearchParams instance toString method
91 // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
92 // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
93
94 // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
95 source = object.toString()
96
97 // Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
98 type = 'application/x-www-form-urlencoded;charset=UTF-8'
99 } else if (isArrayBuffer(object)) {
100 // BufferSource/ArrayBuffer
101
102 // Set source to a copy of the bytes held by object.
103 source = new Uint8Array(object.slice())
104 } else if (ArrayBuffer.isView(object)) {
105 // BufferSource/ArrayBufferView
106
107 // Set source to a copy of the bytes held by object.
108 source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
109 } else if (util.isFormDataLike(object)) {
110 const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}`
111 const prefix = `--${boundary}\r\nContent-Disposition: form-data`
112
113 /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
114 const escape = (str) =>
115 str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
116 const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
117
118 // Set action to this step: run the multipart/form-data
119 // encoding algorithm, with object’s entry list and UTF-8.
120 // - This ensures that the body is immutable and can't be changed afterwords
121 // - That the content-length is calculated in advance.
122 // - And that all parts are pre-encoded and ready to be sent.
123
124 const blobParts = []
125 const rn = new Uint8Array([13, 10]) // '\r\n'
126 length = 0
127 let hasUnknownSizeValue = false
128
129 for (const [name, value] of object) {
130 if (typeof value === 'string') {
131 const chunk = textEncoder.encode(prefix +
132 `; name="${escape(normalizeLinefeeds(name))}"` +
133 `\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
134 blobParts.push(chunk)
135 length += chunk.byteLength
136 } else {
137 const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
138 (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
139 `Content-Type: ${
140 value.type || 'application/octet-stream'
141 }\r\n\r\n`)
142 blobParts.push(chunk, value, rn)
143 if (typeof value.size === 'number') {
144 length += chunk.byteLength + value.size + rn.byteLength
145 } else {
146 hasUnknownSizeValue = true
147 }
148 }
149 }
150
151 const chunk = textEncoder.encode(`--${boundary}--`)
152 blobParts.push(chunk)
153 length += chunk.byteLength
154 if (hasUnknownSizeValue) {
155 length = null
156 }
157
158 // Set source to object.
159 source = object
160
161 action = async function * () {
162 for (const part of blobParts) {
163 if (part.stream) {
164 yield * part.stream()
165 } else {
166 yield part
167 }
168 }
169 }
170
171 // Set type to `multipart/form-data; boundary=`,
172 // followed by the multipart/form-data boundary string generated
173 // by the multipart/form-data encoding algorithm.
174 type = 'multipart/form-data; boundary=' + boundary
175 } else if (isBlobLike(object)) {
176 // Blob
177
178 // Set source to object.
179 source = object
180
181 // Set length to object’s size.
182 length = object.size
183
184 // If object’s type attribute is not the empty byte sequence, set
185 // type to its value.
186 if (object.type) {
187 type = object.type
188 }
189 } else if (typeof object[Symbol.asyncIterator] === 'function') {
190 // If keepalive is true, then throw a TypeError.
191 if (keepalive) {
192 throw new TypeError('keepalive')
193 }
194
195 // If object is disturbed or locked, then throw a TypeError.
196 if (util.isDisturbed(object) || object.locked) {
197 throw new TypeError(
198 'Response body object should not be disturbed or locked'
199 )
200 }
201
202 stream =
203 object instanceof ReadableStream ? object : ReadableStreamFrom(object)
204 }
205
206 // 11. If source is a byte sequence, then set action to a
207 // step that returns source and length to source’s length.
208 if (typeof source === 'string' || util.isBuffer(source)) {
209 length = Buffer.byteLength(source)
210 }
211
212 // 12. If action is non-null, then run these steps in in parallel:
213 if (action != null) {
214 // Run action.
215 let iterator
216 stream = new ReadableStream({
217 async start () {
218 iterator = action(object)[Symbol.asyncIterator]()
219 },
220 async pull (controller) {
221 const { value, done } = await iterator.next()
222 if (done) {
223 // When running action is done, close stream.
224 queueMicrotask(() => {
225 controller.close()
226 })
227 } else {
228 // Whenever one or more bytes are available and stream is not errored,
229 // enqueue a Uint8Array wrapping an ArrayBuffer containing the available
230 // bytes into stream.
231 if (!isErrored(stream)) {
232 controller.enqueue(new Uint8Array(value))
233 }
234 }
235 return controller.desiredSize > 0
236 },
237 async cancel (reason) {
238 await iterator.return()
239 },
240 type: undefined
241 })
242 }
243
244 // 13. Let body be a body whose stream is stream, source is source,
245 // and length is length.
246 const body = { stream, source, length }
247
248 // 14. Return (body, type).
249 return [body, type]
250}
251
252// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
253function safelyExtractBody (object, keepalive = false) {
254 if (!ReadableStream) {
255 // istanbul ignore next
256 ReadableStream = require('stream/web').ReadableStream
257 }
258
259 // To safely extract a body and a `Content-Type` value from
260 // a byte sequence or BodyInit object object, run these steps:
261
262 // 1. If object is a ReadableStream object, then:
263 if (object instanceof ReadableStream) {
264 // Assert: object is neither disturbed nor locked.
265 // istanbul ignore next
266 assert(!util.isDisturbed(object), 'The body has already been consumed.')
267 // istanbul ignore next
268 assert(!object.locked, 'The stream is locked.')
269 }
270
271 // 2. Return the results of extracting object.
272 return extractBody(object, keepalive)
273}
274
275function cloneBody (body) {
276 // To clone a body body, run these steps:
277
278 // https://fetch.spec.whatwg.org/#concept-body-clone
279
280 // 1. Let « out1, out2 » be the result of teeing body’s stream.
281 const [out1, out2] = body.stream.tee()
282 const out2Clone = structuredClone(out2, { transfer: [out2] })
283 // This, for whatever reasons, unrefs out2Clone which allows
284 // the process to exit by itself.
285 const [, finalClone] = out2Clone.tee()
286
287 // 2. Set body’s stream to out1.
288 body.stream = out1
289
290 // 3. Return a body whose stream is out2 and other members are copied from body.
291 return {
292 stream: finalClone,
293 length: body.length,
294 source: body.source
295 }
296}
297
298async function * consumeBody (body) {
299 if (body) {
300 if (isUint8Array(body)) {
301 yield body
302 } else {
303 const stream = body.stream
304
305 if (util.isDisturbed(stream)) {
306 throw new TypeError('The body has already been consumed.')
307 }
308
309 if (stream.locked) {
310 throw new TypeError('The stream is locked.')
311 }
312
313 // Compat.
314 stream[kBodyUsed] = true
315
316 yield * stream
317 }
318 }
319}
320
321function throwIfAborted (state) {
322 if (state.aborted) {
323 throw new DOMException('The operation was aborted.', 'AbortError')
324 }
325}
326
327function bodyMixinMethods (instance) {
328 const methods = {
329 blob () {
330 // The blob() method steps are to return the result of
331 // running consume body with this and the following step
332 // given a byte sequence bytes: return a Blob whose
333 // contents are bytes and whose type attribute is this’s
334 // MIME type.
335 return specConsumeBody(this, (bytes) => {
336 let mimeType = bodyMimeType(this)
337
338 if (mimeType === 'failure') {
339 mimeType = ''
340 } else if (mimeType) {
341 mimeType = serializeAMimeType(mimeType)
342 }
343
344 // Return a Blob whose contents are bytes and type attribute
345 // is mimeType.
346 return new Blob([bytes], { type: mimeType })
347 }, instance)
348 },
349
350 arrayBuffer () {
351 // The arrayBuffer() method steps are to return the result
352 // of running consume body with this and the following step
353 // given a byte sequence bytes: return a new ArrayBuffer
354 // whose contents are bytes.
355 return specConsumeBody(this, (bytes) => {
356 return new Uint8Array(bytes).buffer
357 }, instance)
358 },
359
360 text () {
361 // The text() method steps are to return the result of running
362 // consume body with this and UTF-8 decode.
363 return specConsumeBody(this, utf8DecodeBytes, instance)
364 },
365
366 json () {
367 // The json() method steps are to return the result of running
368 // consume body with this and parse JSON from bytes.
369 return specConsumeBody(this, parseJSONFromBytes, instance)
370 },
371
372 async formData () {
373 webidl.brandCheck(this, instance)
374
375 throwIfAborted(this[kState])
376
377 const contentType = this.headers.get('Content-Type')
378
379 // If mimeType’s essence is "multipart/form-data", then:
380 if (/multipart\/form-data/.test(contentType)) {
381 const headers = {}
382 for (const [key, value] of this.headers) headers[key.toLowerCase()] = value
383
384 const responseFormData = new FormData()
385
386 let busboy
387
388 try {
389 busboy = new Busboy({
390 headers,
391 preservePath: true
392 })
393 } catch (err) {
394 throw new DOMException(`${err}`, 'AbortError')
395 }
396
397 busboy.on('field', (name, value) => {
398 responseFormData.append(name, value)
399 })
400 busboy.on('file', (name, value, filename, encoding, mimeType) => {
401 const chunks = []
402
403 if (encoding === 'base64' || encoding.toLowerCase() === 'base64') {
404 let base64chunk = ''
405
406 value.on('data', (chunk) => {
407 base64chunk += chunk.toString().replace(/[\r\n]/gm, '')
408
409 const end = base64chunk.length - base64chunk.length % 4
410 chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64'))
411
412 base64chunk = base64chunk.slice(end)
413 })
414 value.on('end', () => {
415 chunks.push(Buffer.from(base64chunk, 'base64'))
416 responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
417 })
418 } else {
419 value.on('data', (chunk) => {
420 chunks.push(chunk)
421 })
422 value.on('end', () => {
423 responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
424 })
425 }
426 })
427
428 const busboyResolve = new Promise((resolve, reject) => {
429 busboy.on('finish', resolve)
430 busboy.on('error', (err) => reject(new TypeError(err)))
431 })
432
433 if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk)
434 busboy.end()
435 await busboyResolve
436
437 return responseFormData
438 } else if (/application\/x-www-form-urlencoded/.test(contentType)) {
439 // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
440
441 // 1. Let entries be the result of parsing bytes.
442 let entries
443 try {
444 let text = ''
445 // application/x-www-form-urlencoded parser will keep the BOM.
446 // https://url.spec.whatwg.org/#concept-urlencoded-parser
447 // Note that streaming decoder is stateful and cannot be reused
448 const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
449
450 for await (const chunk of consumeBody(this[kState].body)) {
451 if (!isUint8Array(chunk)) {
452 throw new TypeError('Expected Uint8Array chunk')
453 }
454 text += streamingDecoder.decode(chunk, { stream: true })
455 }
456 text += streamingDecoder.decode()
457 entries = new URLSearchParams(text)
458 } catch (err) {
459 // istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
460 // 2. If entries is failure, then throw a TypeError.
461 throw Object.assign(new TypeError(), { cause: err })
462 }
463
464 // 3. Return a new FormData object whose entries are entries.
465 const formData = new FormData()
466 for (const [name, value] of entries) {
467 formData.append(name, value)
468 }
469 return formData
470 } else {
471 // Wait a tick before checking if the request has been aborted.
472 // Otherwise, a TypeError can be thrown when an AbortError should.
473 await Promise.resolve()
474
475 throwIfAborted(this[kState])
476
477 // Otherwise, throw a TypeError.
478 throw webidl.errors.exception({
479 header: `${instance.name}.formData`,
480 message: 'Could not parse content as FormData.'
481 })
482 }
483 }
484 }
485
486 return methods
487}
488
489function mixinBody (prototype) {
490 Object.assign(prototype.prototype, bodyMixinMethods(prototype))
491}
492
493/**
494 * @see https://fetch.spec.whatwg.org/#concept-body-consume-body
495 * @param {Response|Request} object
496 * @param {(value: unknown) => unknown} convertBytesToJSValue
497 * @param {Response|Request} instance
498 */
499async function specConsumeBody (object, convertBytesToJSValue, instance) {
500 webidl.brandCheck(object, instance)
501
502 throwIfAborted(object[kState])
503
504 // 1. If object is unusable, then return a promise rejected
505 // with a TypeError.
506 if (bodyUnusable(object[kState].body)) {
507 throw new TypeError('Body is unusable')
508 }
509
510 // 2. Let promise be a new promise.
511 const promise = createDeferredPromise()
512
513 // 3. Let errorSteps given error be to reject promise with error.
514 const errorSteps = (error) => promise.reject(error)
515
516 // 4. Let successSteps given a byte sequence data be to resolve
517 // promise with the result of running convertBytesToJSValue
518 // with data. If that threw an exception, then run errorSteps
519 // with that exception.
520 const successSteps = (data) => {
521 try {
522 promise.resolve(convertBytesToJSValue(data))
523 } catch (e) {
524 errorSteps(e)
525 }
526 }
527
528 // 5. If object’s body is null, then run successSteps with an
529 // empty byte sequence.
530 if (object[kState].body == null) {
531 successSteps(new Uint8Array())
532 return promise.promise
533 }
534
535 // 6. Otherwise, fully read object’s body given successSteps,
536 // errorSteps, and object’s relevant global object.
537 await fullyReadBody(object[kState].body, successSteps, errorSteps)
538
539 // 7. Return promise.
540 return promise.promise
541}
542
543// https://fetch.spec.whatwg.org/#body-unusable
544function bodyUnusable (body) {
545 // An object including the Body interface mixin is
546 // said to be unusable if its body is non-null and
547 // its body’s stream is disturbed or locked.
548 return body != null && (body.stream.locked || util.isDisturbed(body.stream))
549}
550
551/**
552 * @see https://encoding.spec.whatwg.org/#utf-8-decode
553 * @param {Buffer} buffer
554 */
555function utf8DecodeBytes (buffer) {
556 if (buffer.length === 0) {
557 return ''
558 }
559
560 // 1. Let buffer be the result of peeking three bytes from
561 // ioQueue, converted to a byte sequence.
562
563 // 2. If buffer is 0xEF 0xBB 0xBF, then read three
564 // bytes from ioQueue. (Do nothing with those bytes.)
565 if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
566 buffer = buffer.subarray(3)
567 }
568
569 // 3. Process a queue with an instance of UTF-8’s
570 // decoder, ioQueue, output, and "replacement".
571 const output = textDecoder.decode(buffer)
572
573 // 4. Return output.
574 return output
575}
576
577/**
578 * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
579 * @param {Uint8Array} bytes
580 */
581function parseJSONFromBytes (bytes) {
582 return JSON.parse(utf8DecodeBytes(bytes))
583}
584
585/**
586 * @see https://fetch.spec.whatwg.org/#concept-body-mime-type
587 * @param {import('./response').Response|import('./request').Request} object
588 */
589function bodyMimeType (object) {
590 const { headersList } = object[kState]
591 const contentType = headersList.get('content-type')
592
593 if (contentType === null) {
594 return 'failure'
595 }
596
597 return parseMIMEType(contentType)
598}
599
600module.exports = {
601 extractBody,
602 safelyExtractBody,
603 cloneBody,
604 mixinBody
605}
Note: See TracBrowser for help on using the repository browser.