Skip to content

Commit

Permalink
make fs body immutable and encode only once (nodejs#1814)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmywarting authored and crysmags committed Feb 27, 2024
1 parent 4016e6c commit 9d4f817
Showing 1 changed file with 35 additions and 54 deletions.
89 changes: 35 additions & 54 deletions lib/fetch/body.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function extractBody (object, keepalive = false) {
// Set source to a copy of the bytes held by object.
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
} else if (util.isFormDataLike(object)) {
const boundary = '----formdata-undici-' + Math.random()
const boundary = `----formdata-undici-${Math.random()}`.replace('.', '').slice(0, 32)
const prefix = `--${boundary}\r\nContent-Disposition: form-data`

/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
Expand All @@ -109,68 +109,49 @@ function extractBody (object, keepalive = false) {

// Set action to this step: run the multipart/form-data
// encoding algorithm, with object’s entry list and UTF-8.
action = async function * (object) {
const enc = new TextEncoder()

for (const [name, value] of object) {
if (typeof value === 'string') {
yield enc.encode(
prefix +
`; name="${escape(normalizeLinefeeds(name))}"` +
`\r\n\r\n${normalizeLinefeeds(value)}\r\n`
)
} else {
yield enc.encode(
prefix +
`; name="${escape(normalizeLinefeeds(name))}"` +
(value.name ? `; filename="${escape(value.name)}"` : '') +
'\r\n' +
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`
)

yield * value.stream()

// '\r\n' encoded
yield new Uint8Array([13, 10])
}
// - 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 enc = new TextEncoder()
const blobParts = []
const rn = new Uint8Array([13, 10]) // '\r\n'
length = 0

for (const [name, value] of object) {
if (typeof value === 'string') {
const chunk = enc.encode(prefix +
`; name="${escape(normalizeLinefeeds(name))}"` +
`\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
blobParts.push(chunk)
length += chunk.byteLength
} else {
const chunk = enc.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
(value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`)
blobParts.push(chunk, value, rn)
length += chunk.byteLength + value.size + rn.byteLength
}

yield enc.encode(`--${boundary}--`)
}

const chunk = enc.encode(`--${boundary}--`)
blobParts.push(chunk)
length += chunk.byteLength

// Set source to object.
source = object

// Set length to unclear, see html/6424 for improving this.
length = (() => {
const prefixLength = prefix.length
const boundaryLength = boundary.length
let bodyLength = 0

for (const [name, value] of object) {
if (typeof value === 'string') {
bodyLength +=
prefixLength +
Buffer.byteLength(`; name="${escape(normalizeLinefeeds(name))}"\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
action = async function * () {
for (const part of blobParts) {
if (part.stream) {
yield * part.stream()
} else {
bodyLength +=
prefixLength +
Buffer.byteLength(`; name="${escape(normalizeLinefeeds(name))}"` + (value.name ? `; filename="${escape(value.name)}"` : '')) +
2 + // \r\n
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`.length

// value is a Blob or File, and \r\n
bodyLength += value.size + 2
yield part
}
}

bodyLength += boundaryLength + 4 // --boundary--
return bodyLength
})()
}

// Set type to `multipart/form-data; boundary=`,
// followed by the multipart/form-data boundary string generated
Expand Down

0 comments on commit 9d4f817

Please sign in to comment.