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