Skip to content

Commit ca5a7a2

Browse files
committed
make req.body[name] an array
1 parent ff96362 commit ca5a7a2

File tree

6 files changed

+106
-86
lines changed

6 files changed

+106
-86
lines changed

.vscode/settings.json

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
{
2+
"npm.packageManager": "pnpm",
3+
"editor.formatOnSave": true,
24
"biome.enabled": true,
3-
"editor.defaultFormatter": "biomejs.biome",
4-
"prettier.enable": false,
55
"eslint.enable": false,
6+
"prettier.enable": false,
67
"editor.codeActionsOnSave": {
7-
"source.fixAll": "always"
8-
},
9-
"typescript.tsdk": "node_modules/typescript/lib",
10-
"[typescript]": {
11-
"editor.defaultFormatter": "biomejs.biome"
8+
"source.fixAll": "explicit",
9+
"source.organizeImports.biome": "explicit"
1210
},
13-
"[javascript]": {
14-
"editor.defaultFormatter": "biomejs.biome"
15-
}
16-
}
11+
"typescript.tsdk": "node_modules/typescript/lib"
12+
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Parses request body using `multipart/form-data` content type and boundary. Suppo
9090
```js
9191
// curl -F "textfield=textfield" -F "someother=textfield with text" localhost:3000
9292
await multipart()(req, res, (err) => void err && console.log(err))
93-
res.end(req.body) // { textfield: "textfield", someother: "textfield with text" }
93+
res.end(req.body) // { textfield: ["textfield"], someother: ["textfield with text"] }
9494
```
9595

9696
### `custom(fn)(req, res, cb)`

bench/formidable.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1+
import { createReadStream } from 'node:fs'
12
// @ts-check
23
import { createServer } from 'node:http'
34
import formidable from 'formidable'
4-
import { createReadStream } from 'node:fs'
55

66
const form = formidable({})
77

88
const server = createServer((req, res) => {
9-
form.parse(req, (_, __, files) => {
9+
form.parse(req, (_, fields, files) => {
1010
// @ts-expect-error this is JS
1111
const file = createReadStream(files.file[0].filepath)
12-
1312
file.pipe(res)
1413
})
1514
})
1615

17-
server.listen(3005)
16+
server.listen(3005)

bench/milliparsec-multipart.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ const server = createServer((req, res) => {
1010
* @type {File}
1111
*/
1212
// @ts-ignore
13-
const file = req.body.file
14-
13+
const file = req.body.file[0]
1514
const stream = file.stream()
1615

1716
res.writeHead(200, {

src/index.ts

Lines changed: 61 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -24,66 +24,66 @@ const defaultErrorFn: LimitErrorFn = (limit) => `Payload too large. Limit: ${lim
2424
// Main function
2525
export const p =
2626
<T = any>(fn: (body: any) => any, limit = defaultPayloadLimit, errorFn: LimitErrorFn = defaultErrorFn) =>
27-
async (req: ReqWithBody<T>, _res: Response, next: (err?: any) => void) => {
28-
try {
29-
let body = ''
30-
31-
for await (const chunk of req) {
32-
if (body.length > limit) throw new Error(errorFn(limit))
33-
body += chunk
27+
async (req: ReqWithBody<T>, _res: Response, next: (err?: any) => void) => {
28+
try {
29+
let body = ''
30+
31+
for await (const chunk of req) {
32+
if (body.length > limit) throw new Error(errorFn(limit))
33+
body += chunk
34+
}
35+
36+
return fn(body)
37+
} catch (e) {
38+
next(e)
3439
}
35-
36-
return fn(body)
37-
} catch (e) {
38-
next(e)
3940
}
40-
}
4141

4242
const custom =
4343
<T = any>(fn: (body: any) => any) =>
44-
async (req: ReqWithBody, _res: Response, next: NextFunction) => {
45-
if (hasBody(req.method!)) req.body = await p<T>(fn)(req, _res, next)
46-
next()
47-
}
44+
async (req: ReqWithBody, _res: Response, next: NextFunction) => {
45+
if (hasBody(req.method!)) req.body = await p<T>(fn)(req, _res, next)
46+
next()
47+
}
4848

4949
const json =
5050
({ limit, errorFn }: ParserOptions = {}) =>
51-
async (req: ReqWithBody, res: Response, next: NextFunction) => {
52-
if (hasBody(req.method!)) {
53-
req.body = await p((x) => (x ? JSON.parse(x.toString()) : {}), limit, errorFn)(req, res, next)
54-
} else next()
55-
}
51+
async (req: ReqWithBody, res: Response, next: NextFunction) => {
52+
if (hasBody(req.method!)) {
53+
req.body = await p((x) => (x ? JSON.parse(x.toString()) : {}), limit, errorFn)(req, res, next)
54+
} else next()
55+
}
5656

5757
const raw =
5858
({ limit, errorFn }: ParserOptions = {}) =>
59-
async (req: ReqWithBody, _res: Response, next: NextFunction) => {
60-
if (hasBody(req.method!)) {
61-
req.body = await p((x) => x, limit, errorFn)(req, _res, next)
62-
} else next()
63-
}
59+
async (req: ReqWithBody, _res: Response, next: NextFunction) => {
60+
if (hasBody(req.method!)) {
61+
req.body = await p((x) => x, limit, errorFn)(req, _res, next)
62+
} else next()
63+
}
6464

6565
const text =
6666
({ limit, errorFn }: ParserOptions = {}) =>
67-
async (req: ReqWithBody, _res: Response, next: NextFunction) => {
68-
if (hasBody(req.method!)) {
69-
req.body = await p((x) => x.toString(), limit, errorFn)(req, _res, next)
70-
} else next()
71-
}
67+
async (req: ReqWithBody, _res: Response, next: NextFunction) => {
68+
if (hasBody(req.method!)) {
69+
req.body = await p((x) => x.toString(), limit, errorFn)(req, _res, next)
70+
} else next()
71+
}
7272

7373
const urlencoded =
7474
({ limit, errorFn }: ParserOptions = {}) =>
75-
async (req: ReqWithBody, _res: Response, next: NextFunction) => {
76-
if (hasBody(req.method!)) {
77-
req.body = await p(
78-
(x) => {
79-
const urlSearchParam = new URLSearchParams(x.toString())
80-
return Object.fromEntries(urlSearchParam.entries())
81-
},
82-
limit,
83-
errorFn
84-
)(req, _res, next)
85-
} else next()
86-
}
75+
async (req: ReqWithBody, _res: Response, next: NextFunction) => {
76+
if (hasBody(req.method!)) {
77+
req.body = await p(
78+
(x) => {
79+
const urlSearchParam = new URLSearchParams(x.toString())
80+
return Object.fromEntries(urlSearchParam.entries())
81+
},
82+
limit,
83+
errorFn
84+
)(req, _res, next)
85+
} else next()
86+
}
8787

8888
const getBoundary = (contentType: string) => {
8989
// Extract the boundary from the Content-Type header
@@ -94,9 +94,10 @@ const getBoundary = (contentType: string) => {
9494
const parseMultipart = (body: string, boundary: string) => {
9595
// Split the body into an array of parts
9696
const parts = body.split(new RegExp(`${boundary}(--)?`)).filter((part) => !!part && /content-disposition/i.test(part))
97-
const parsedBody = {}
97+
const parsedBody: Record<string, (File | string)[]> = {}
9898
// Parse each part into a form data object
99-
parts.map((part) => {
99+
// biome-ignore lint/complexity/noForEach: <explanation>
100+
parts.forEach((part) => {
100101
const [headers, ...lines] = part.split('\r\n').filter((part) => !!part)
101102
const data = lines.join('\r\n').trim()
102103

@@ -107,31 +108,33 @@ const parseMultipart = (body: string, boundary: string) => {
107108
const contentTypeMatch = /Content-Type: (.+)/i.exec(data)!
108109
const fileContent = data.slice(contentTypeMatch[0].length + 2)
109110

110-
return Object.assign(parsedBody, {
111-
[name]: new File([fileContent], filename[1], { type: contentTypeMatch[1] })
112-
})
111+
const file = new File([fileContent], filename[1], { type: contentTypeMatch[1] })
112+
113+
parsedBody[name] = parsedBody[name] ? [...parsedBody[name], file] : [file]
114+
return
113115
}
114116
// This is a regular field
115-
return Object.assign(parsedBody, { [name]: data })
117+
parsedBody[name] = parsedBody[name] ? [...parsedBody[name], data] : [data]
118+
return
116119
})
117120

118121
return parsedBody
119122
}
120-
121123
type MultipartOptions = Partial<{
122124
fileCountLimit: number
123125
fileSizeLimit: number
124126
}>
125127

126128
const multipart =
127129
(opts: MultipartOptions = {}) =>
128-
async (req: ReqWithBody, res: Response, next: NextFunction) => {
129-
if (hasBody(req.method!)) {
130-
req.body = await p((x) => {
131-
const boundary = getBoundary(req.headers['content-type']!)
132-
if (boundary) return parseMultipart(x, boundary)
133-
})(req, res, next)
134-
} else next()
135-
}
130+
async (req: ReqWithBody, res: Response, next: NextFunction) => {
131+
if (hasBody(req.method!)) {
132+
req.body = await p((x) => {
133+
const boundary = getBoundary(req.headers['content-type']!)
134+
if (boundary) return parseMultipart(x, boundary)
135+
})(req, res, next)
136+
next()
137+
} else next()
138+
}
136139

137140
export { custom, json, raw, text, urlencoded, multipart }

test.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,8 @@ test('should parse multipart body', async () => {
261261
body: fd,
262262
method: 'POST'
263263
}).expect(200, {
264-
textfield: 'textfield data\r\nwith new lines\r\nbecause this is valid',
265-
someother: 'textfield with text'
264+
textfield: ['textfield data\r\nwith new lines\r\nbecause this is valid'],
265+
someother: ['textfield with text']
266266
})
267267
})
268268

@@ -284,8 +284,31 @@ test('should parse multipart with boundary', async () => {
284284
'Content-Type': 'multipart/form-data; boundary=some-boundary'
285285
}
286286
}).expect(200, {
287-
textfield: 'textfield data\nwith new lines\nbecause this is valid',
288-
someother: 'textfield with text'
287+
textfield: ['textfield data\nwith new lines\nbecause this is valid'],
288+
someother: ['textfield with text']
289+
})
290+
})
291+
292+
test('should parse an array of multipart values', async () => {
293+
const server = createServer(async (req: ReqWithBody, res) => {
294+
await multipart()(req, res, (err) => void err && console.log(err))
295+
296+
res.setHeader('Content-Type', 'multipart/form-data; boundary=some-boundary')
297+
298+
res.end(JSON.stringify(req.body))
299+
})
300+
301+
const fd = new FormData()
302+
303+
fd.set('textfield', 'textfield data\nwith new lines\nbecause this is valid')
304+
fd.append('textfield', 'textfield with text')
305+
306+
await makeFetch(server)('/', {
307+
// probaly better to use form-data package
308+
body: fd,
309+
method: 'POST'
310+
}).expect(200, {
311+
textfield: ['textfield data\r\nwith new lines\r\nbecause this is valid', 'textfield with text'],
289312
})
290313
})
291314

@@ -311,17 +334,17 @@ test('should parse multipart with files', async () => {
311334
const fd = new FormData()
312335
const file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })
313336
fd.set('file', file)
314-
const server = createServer(async (req: ReqWithBody<{ file: File }>, res) => {
337+
const server = createServer(async (req: ReqWithBody<{ file: [File] }>, res) => {
315338
await multipart()(req, res, (err) => void err && console.log(err))
316339

317340
res.setHeader('Content-Type', 'multipart/form-data')
318341

319342
const formBuf = new Uint8Array(await file.arrayBuffer())
320-
const buf = new Uint8Array(await (req.body?.file as File).arrayBuffer())
343+
const buf = new Uint8Array(await (req.body!.file[0]).arrayBuffer())
321344

322345
assert.equal(Buffer.compare(buf, formBuf), 0)
323346

324-
res.end(req.body?.file.name)
347+
res.end(req.body?.file[0].name)
325348
})
326349

327350
await makeFetch(server)('/', {
@@ -342,17 +365,17 @@ test('should support multiple files', async () => {
342365
fd.set('file1', files[0])
343366
fd.set('file2', files[1])
344367

345-
const server = createServer(async (req: ReqWithBody<{ file1: File; file2: File }>, res) => {
368+
const server = createServer(async (req: ReqWithBody<{ file1: [File]; file2: [File] }>, res) => {
346369
await multipart()(req, res, (err) => void err && console.log(err))
347370

348371
res.setHeader('Content-Type', 'multipart/form-data')
349372

350373
const files = Object.values(req.body!)
351374

352375
for (const file of files) {
353-
const buf = new Uint8Array(await file.arrayBuffer())
376+
const buf = new Uint8Array(await file[0].arrayBuffer())
354377
const i = files.indexOf(file)
355-
const formBuf = new Uint8Array(await files[i].arrayBuffer())
378+
const formBuf = new Uint8Array(await files[i][0].arrayBuffer())
356379
assert.strictEqual(Buffer.compare(buf, formBuf), 0)
357380
}
358381
res.end('ok')

0 commit comments

Comments
 (0)