body.js 19 KB

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