diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 920d873f2cb..3212631fb9b 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -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 */ @@ -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