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} 606