body.js 17 KB

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