Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make formdata body immutable and encode it only once #1814

Merged
merged 1 commit into from
Dec 18, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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