Spaces:
Paused
Paused
File size: 17,544 Bytes
3401f26 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 | 'use strict'
const util = require('../../core/util')
const {
ReadableStreamFrom,
readableStreamClose,
fullyReadBody,
extractMimeType
} = require('./util')
const { FormData, setFormDataState } = require('./formdata')
const { webidl } = require('../webidl')
const assert = require('node:assert')
const { isErrored, isDisturbed } = require('node:stream')
const { isUint8Array } = require('node:util/types')
const { serializeAMimeType } = require('./data-url')
const { multipartFormDataParser } = require('./formdata-parser')
const { createDeferredPromise } = require('../../util/promise')
const { parseJSONFromBytes } = require('../infra')
const { utf8DecodeBytes } = require('../../encoding')
const { runtimeFeatures } = require('../../util/runtime-features.js')
const random = runtimeFeatures.has('crypto')
? require('node:crypto').randomInt
: (max) => Math.floor(Math.random() * max)
const textEncoder = new TextEncoder()
function noop () {}
const streamRegistry = new FinalizationRegistry((weakRef) => {
const stream = weakRef.deref()
if (stream && !stream.locked && !isDisturbed(stream) && !isErrored(stream)) {
stream.cancel('Response object has been garbage collected').catch(noop)
}
})
/**
* Extract a body with type from a byte sequence or BodyInit object
*
* @param {import('../../../types').BodyInit} object - The BodyInit object to extract from
* @param {boolean} [keepalive=false] - If true, indicates that the body
* @returns {[{stream: ReadableStream, source: any, length: number | null}, string | null]} - Returns a tuple containing the body and its type
*
* @see https://fetch.spec.whatwg.org/#concept-bodyinit-extract
*/
function extractBody (object, keepalive = false) {
// 1. Let stream be null.
let stream = null
let controller = null
// 2. If object is a ReadableStream object, then set stream to object.
if (webidl.is.ReadableStream(object)) {
stream = object
} else if (webidl.is.Blob(object)) {
// 3. Otherwise, if object is a Blob object, set stream to the
// result of running object’s get stream.
stream = object.stream()
} else {
// 4. Otherwise, set stream to a new ReadableStream object, and set
// up stream with byte reading support.
stream = new ReadableStream({
pull () {},
start (c) {
controller = c
},
cancel () {},
type: 'bytes'
})
}
// 5. Assert: stream is a ReadableStream object.
assert(webidl.is.ReadableStream(stream))
// 6. Let action be null.
let action = null
// 7. Let source be null.
let source = null
// 8. Let length be null.
let length = null
// 9. Let type be null.
let type = null
// 10. Switch on object:
if (typeof object === 'string') {
// Set source to the UTF-8 encoding of object.
// Note: setting source to a Uint8Array here breaks some mocking assumptions.
source = object
// Set type to `text/plain;charset=UTF-8`.
type = 'text/plain;charset=UTF-8'
} else if (webidl.is.URLSearchParams(object)) {
// URLSearchParams
// spec says to run application/x-www-form-urlencoded on body.list
// this is implemented in Node.js as apart of an URLSearchParams instance toString method
// See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
// and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
// Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
source = object.toString()
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
type = 'application/x-www-form-urlencoded;charset=UTF-8'
} else if (webidl.is.BufferSource(object)) {
// Set source to a copy of the bytes held by object.
source = webidl.util.getCopyOfBytesHeldByBufferSource(object)
} else if (webidl.is.FormData(object)) {
const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
const formdataEscape = (str) =>
str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
// Set action to this step: run the multipart/form-data
// encoding algorithm, with object’s entry list and UTF-8.
// - This ensures that the body is immutable and can't be changed afterwords
// - That the content-length is calculated in advance.
// - And that all parts are pre-encoded and ready to be sent.
const blobParts = []
const rn = new Uint8Array([13, 10]) // '\r\n'
length = 0
let hasUnknownSizeValue = false
for (const [name, value] of object) {
if (typeof value === 'string') {
const chunk = textEncoder.encode(prefix +
`; name="${formdataEscape(normalizeLinefeeds(name))}"` +
`\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
blobParts.push(chunk)
length += chunk.byteLength
} else {
const chunk = textEncoder.encode(`${prefix}; name="${formdataEscape(normalizeLinefeeds(name))}"` +
(value.name ? `; filename="${formdataEscape(value.name)}"` : '') + '\r\n' +
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`)
blobParts.push(chunk, value, rn)
if (typeof value.size === 'number') {
length += chunk.byteLength + value.size + rn.byteLength
} else {
hasUnknownSizeValue = true
}
}
}
// CRLF is appended to the body to function with legacy servers and match other implementations.
// https://github.com/curl/curl/blob/3434c6b46e682452973972e8313613dfa58cd690/lib/mime.c#L1029-L1030
// https://github.com/form-data/form-data/issues/63
const chunk = textEncoder.encode(`--${boundary}--\r\n`)
blobParts.push(chunk)
length += chunk.byteLength
if (hasUnknownSizeValue) {
length = null
}
// Set source to object.
source = object
action = async function * () {
for (const part of blobParts) {
if (part.stream) {
yield * part.stream()
} else {
yield part
}
}
}
// Set type to `multipart/form-data; boundary=`,
// followed by the multipart/form-data boundary string generated
// by the multipart/form-data encoding algorithm.
type = `multipart/form-data; boundary=${boundary}`
} else if (webidl.is.Blob(object)) {
// Blob
// Set source to object.
source = object
// Set length to object’s size.
length = object.size
// If object’s type attribute is not the empty byte sequence, set
// type to its value.
if (object.type) {
type = object.type
}
} else if (typeof object[Symbol.asyncIterator] === 'function') {
// If keepalive is true, then throw a TypeError.
if (keepalive) {
throw new TypeError('keepalive')
}
// If object is disturbed or locked, then throw a TypeError.
if (util.isDisturbed(object) || object.locked) {
throw new TypeError(
'Response body object should not be disturbed or locked'
)
}
stream =
webidl.is.ReadableStream(object) ? object : ReadableStreamFrom(object)
}
// 11. If source is a byte sequence, then set action to a
// step that returns source and length to source’s length.
if (typeof source === 'string' || isUint8Array(source)) {
action = () => {
length = typeof source === 'string' ? Buffer.byteLength(source) : source.length
return source
}
}
// 12. If action is non-null, then run these steps in parallel:
if (action != null) {
;(async () => {
// 1. Run action.
const result = action()
// 2. Whenever one or more bytes are available and stream is not errored,
// enqueue the result of creating a Uint8Array from the available bytes into stream.
const iterator = result?.[Symbol.asyncIterator]?.()
if (iterator) {
for await (const bytes of iterator) {
if (isErrored(stream)) break
if (bytes.length) {
controller.enqueue(new Uint8Array(bytes))
}
}
} else if (result?.length && !isErrored(stream)) {
controller.enqueue(typeof result === 'string' ? textEncoder.encode(result) : new Uint8Array(result))
}
// 3. When running action is done, close stream.
queueMicrotask(() => readableStreamClose(controller))
})()
}
// 13. Let body be a body whose stream is stream, source is source,
// and length is length.
const body = { stream, source, length }
// 14. Return (body, type).
return [body, type]
}
/**
* @typedef {object} ExtractBodyResult
* @property {ReadableStream<Uint8Array<ArrayBuffer>>} stream - The ReadableStream containing the body data
* @property {any} source - The original source of the body data
* @property {number | null} length - The length of the body data, or null
*/
/**
* Safely extract a body with type from a byte sequence or BodyInit object.
*
* @param {import('../../../types').BodyInit} object - The BodyInit object to extract from
* @param {boolean} [keepalive=false] - If true, indicates that the body
* @returns {[ExtractBodyResult, string | null]} - Returns a tuple containing the body and its type
*
* @see https://fetch.spec.whatwg.org/#bodyinit-safely-extract
*/
function safelyExtractBody (object, keepalive = false) {
// To safely extract a body and a `Content-Type` value from
// a byte sequence or BodyInit object object, run these steps:
// 1. If object is a ReadableStream object, then:
if (webidl.is.ReadableStream(object)) {
// Assert: object is neither disturbed nor locked.
assert(!util.isDisturbed(object), 'The body has already been consumed.')
assert(!object.locked, 'The stream is locked.')
}
// 2. Return the results of extracting object.
return extractBody(object, keepalive)
}
function cloneBody (body) {
// To clone a body body, run these steps:
// https://fetch.spec.whatwg.org/#concept-body-clone
// 1. Let « out1, out2 » be the result of teeing body’s stream.
const { 0: out1, 1: out2 } = body.stream.tee()
// 2. Set body’s stream to out1.
body.stream = out1
// 3. Return a body whose stream is out2 and other members are copied from body.
return {
stream: out2,
length: body.length,
source: body.source
}
}
function bodyMixinMethods (instance, getInternalState) {
const methods = {
blob () {
// The blob() method steps are to return the result of
// running consume body with this and the following step
// given a byte sequence bytes: return a Blob whose
// contents are bytes and whose type attribute is this’s
// MIME type.
return consumeBody(this, (bytes) => {
let mimeType = bodyMimeType(getInternalState(this))
if (mimeType === null) {
mimeType = ''
} else if (mimeType) {
mimeType = serializeAMimeType(mimeType)
}
// Return a Blob whose contents are bytes and type attribute
// is mimeType.
return new Blob([bytes], { type: mimeType })
}, instance, getInternalState)
},
arrayBuffer () {
// The arrayBuffer() method steps are to return the result
// of running consume body with this and the following step
// given a byte sequence bytes: return a new ArrayBuffer
// whose contents are bytes.
return consumeBody(this, (bytes) => {
return new Uint8Array(bytes).buffer
}, instance, getInternalState)
},
text () {
// The text() method steps are to return the result of running
// consume body with this and UTF-8 decode.
return consumeBody(this, utf8DecodeBytes, instance, getInternalState)
},
json () {
// The json() method steps are to return the result of running
// consume body with this and parse JSON from bytes.
return consumeBody(this, parseJSONFromBytes, instance, getInternalState)
},
formData () {
// The formData() method steps are to return the result of running
// consume body with this and the following step given a byte sequence bytes:
return consumeBody(this, (value) => {
// 1. Let mimeType be the result of get the MIME type with this.
const mimeType = bodyMimeType(getInternalState(this))
// 2. If mimeType is non-null, then switch on mimeType’s essence and run
// the corresponding steps:
if (mimeType !== null) {
switch (mimeType.essence) {
case 'multipart/form-data': {
// 1. ... [long step]
// 2. If that fails for some reason, then throw a TypeError.
const parsed = multipartFormDataParser(value, mimeType)
// 3. Return a new FormData object, appending each entry,
// resulting from the parsing operation, to its entry list.
const fd = new FormData()
setFormDataState(fd, parsed)
return fd
}
case 'application/x-www-form-urlencoded': {
// 1. Let entries be the result of parsing bytes.
const entries = new URLSearchParams(value.toString())
// 2. If entries is failure, then throw a TypeError.
// 3. Return a new FormData object whose entry list is entries.
const fd = new FormData()
for (const [name, value] of entries) {
fd.append(name, value)
}
return fd
}
}
}
// 3. Throw a TypeError.
throw new TypeError(
'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
)
}, instance, getInternalState)
},
bytes () {
// The bytes() method steps are to return the result of running consume body
// with this and the following step given a byte sequence bytes: return the
// result of creating a Uint8Array from bytes in this’s relevant realm.
return consumeBody(this, (bytes) => {
return new Uint8Array(bytes)
}, instance, getInternalState)
}
}
return methods
}
function mixinBody (prototype, getInternalState) {
Object.assign(prototype.prototype, bodyMixinMethods(prototype, getInternalState))
}
/**
* @see https://fetch.spec.whatwg.org/#concept-body-consume-body
* @param {any} object internal state
* @param {(value: unknown) => unknown} convertBytesToJSValue
* @param {any} instance
* @param {(target: any) => any} getInternalState
*/
function consumeBody (object, convertBytesToJSValue, instance, getInternalState) {
try {
webidl.brandCheck(object, instance)
} catch (e) {
return Promise.reject(e)
}
object = getInternalState(object)
// 1. If object is unusable, then return a promise rejected
// with a TypeError.
if (bodyUnusable(object)) {
return Promise.reject(new TypeError('Body is unusable: Body has already been read'))
}
// 2. Let promise be a new promise.
const promise = createDeferredPromise()
// 3. Let errorSteps given error be to reject promise with error.
const errorSteps = promise.reject
// 4. Let successSteps given a byte sequence data be to resolve
// promise with the result of running convertBytesToJSValue
// with data. If that threw an exception, then run errorSteps
// with that exception.
const successSteps = (data) => {
try {
promise.resolve(convertBytesToJSValue(data))
} catch (e) {
errorSteps(e)
}
}
// 5. If object’s body is null, then run successSteps with an
// empty byte sequence.
if (object.body == null) {
successSteps(Buffer.allocUnsafe(0))
return promise.promise
}
// 6. Otherwise, fully read object’s body given successSteps,
// errorSteps, and object’s relevant global object.
fullyReadBody(object.body, successSteps, errorSteps)
// 7. Return promise.
return promise.promise
}
/**
* @see https://fetch.spec.whatwg.org/#body-unusable
* @param {any} object internal state
*/
function bodyUnusable (object) {
const body = object.body
// An object including the Body interface mixin is
// said to be unusable if its body is non-null and
// its body’s stream is disturbed or locked.
return body != null && (body.stream.locked || util.isDisturbed(body.stream))
}
/**
* @see https://fetch.spec.whatwg.org/#concept-body-mime-type
* @param {any} requestOrResponse internal state
*/
function bodyMimeType (requestOrResponse) {
// 1. Let headers be null.
// 2. If requestOrResponse is a Request object, then set headers to requestOrResponse’s request’s header list.
// 3. Otherwise, set headers to requestOrResponse’s response’s header list.
/** @type {import('./headers').HeadersList} */
const headers = requestOrResponse.headersList
// 4. Let mimeType be the result of extracting a MIME type from headers.
const mimeType = extractMimeType(headers)
// 5. If mimeType is failure, then return null.
if (mimeType === 'failure') {
return null
}
// 6. Return mimeType.
return mimeType
}
module.exports = {
extractBody,
safelyExtractBody,
cloneBody,
mixinBody,
streamRegistry,
bodyUnusable
}
|