Skip to content

Commit df5ded9

Browse files
authored
cache formdata boundary (#5292)
1 parent e101dcb commit df5ded9

3 files changed

Lines changed: 61 additions & 9 deletions

File tree

lib/web/fetch/body.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const {
77
fullyReadBody,
88
extractMimeType
99
} = require('./util')
10-
const { FormData, setFormDataState } = require('./formdata')
10+
const { FormData, setFormDataState, getFormDataBoundary } = require('./formdata')
1111
const { webidl } = require('../webidl')
1212
const assert = require('node:assert')
1313
const { isErrored, isDisturbed } = require('node:stream')
@@ -16,11 +16,6 @@ const { serializeAMimeType } = require('./data-url')
1616
const { multipartFormDataParser } = require('./formdata-parser')
1717
const { parseJSONFromBytes } = require('../infra')
1818
const { utf8DecodeBytes } = require('../../encoding')
19-
const { runtimeFeatures } = require('../../util/runtime-features.js')
20-
21-
const random = runtimeFeatures.has('crypto')
22-
? require('node:crypto').randomInt
23-
: (max) => Math.floor(Math.random() * max)
2419

2520
const textEncoder = new TextEncoder()
2621
function noop () {}
@@ -106,7 +101,7 @@ function extractBody (object, keepalive = false) {
106101
// Set source to a copy of the bytes held by object.
107102
source = webidl.util.getCopyOfBytesHeldByBufferSource(object)
108103
} else if (webidl.is.FormData(object)) {
109-
const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
104+
const boundary = getFormDataBoundary(object)
110105
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
111106

112107
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */

lib/web/fetch/formdata.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ const { iteratorMixin } = require('./util')
44
const { kEnumerableProperty } = require('../../core/util')
55
const { webidl } = require('../webidl')
66
const nodeUtil = require('node:util')
7+
const { runtimeFeatures } = require('../../util/runtime-features.js')
8+
9+
const random = runtimeFeatures.has('crypto')
10+
? require('node:crypto').randomInt
11+
: (max) => Math.floor(Math.random() * max)
712

813
// https://xhr.spec.whatwg.org/#formdata
914
class FormData {
1015
#state = []
16+
#boundary = null
1117

1218
constructor (form = undefined) {
1319
webidl.util.markAsUncloneable(this)
@@ -192,11 +198,24 @@ class FormData {
192198
static setFormDataState (formData, newState) {
193199
formData.#state = newState
194200
}
201+
202+
/**
203+
* @param {FormData} formData
204+
* @returns {string | null}
205+
*/
206+
static getFormDataBoundary (formData) {
207+
const boundary = formData.#boundary
208+
if (boundary != null) return boundary
209+
210+
// eslint-disable-next-line no-return-assign
211+
return formData.#boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
212+
}
195213
}
196214

197-
const { getFormDataState, setFormDataState } = FormData
215+
const { getFormDataState, setFormDataState, getFormDataBoundary } = FormData
198216
Reflect.deleteProperty(FormData, 'getFormDataState')
199217
Reflect.deleteProperty(FormData, 'setFormDataState')
218+
Reflect.deleteProperty(FormData, 'getFormDataBoundary')
200219

201220
iteratorMixin('FormData', FormData, getFormDataState, 'name', 'value')
202221

@@ -256,4 +275,4 @@ function makeEntry (name, value, filename) {
256275

257276
webidl.is.FormData = webidl.util.MakeTypeAssertion(FormData)
258277

259-
module.exports = { FormData, makeEntry, setFormDataState }
278+
module.exports = { FormData, makeEntry, setFormDataState, getFormDataBoundary }

test/fetch/issue-4065.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict'
2+
3+
const { test } = require('node:test')
4+
const { once } = require('node:events')
5+
const { createServer } = require('node:http')
6+
const { fetch, FormData } = require('../..')
7+
8+
// https://github.com/nodejs/undici/issues/4065
9+
test('multipart/form-data boundary is stable across a 307 redirect', async (t) => {
10+
const server = createServer((req, res) => {
11+
if (req.method === 'POST' && req.url === '/first') {
12+
res.writeHead(307, {
13+
Location: `http://localhost:${server.address().port}/second`
14+
})
15+
res.end()
16+
return
17+
}
18+
19+
if (req.method === 'POST' && req.url === '/second') {
20+
res.setHeader('content-type', req.headers['content-type'])
21+
req.pipe(res).on('end', () => res.end())
22+
}
23+
}).listen(0)
24+
25+
t.after(server.close.bind(server))
26+
await once(server, 'listening')
27+
28+
const formData = new FormData()
29+
formData.append('test', 'data')
30+
31+
const response = await fetch(`http://localhost:${server.address().port}/first`, {
32+
method: 'POST',
33+
body: formData,
34+
redirect: 'follow'
35+
})
36+
37+
await t.assert.doesNotReject(response.formData())
38+
})

0 commit comments

Comments
 (0)