body.js 17 KB

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