Skip to content

Commit da7ce60

Browse files
mcollinagurgunday
andauthored
Add FormData decorator to request (#522)
* Add FormData decorator to request Signed-off-by: Matteo Collina <hello@matteocollina.com> * fixup Signed-off-by: Matteo Collina <hello@matteocollina.com> * fixup Signed-off-by: Matteo Collina <hello@matteocollina.com> * Update index.js Co-authored-by: Gürgün Dayıoğlu <hey@gurgun.day> Signed-off-by: Matteo Collina <matteo.collina@gmail.com> * added types Signed-off-by: Matteo Collina <hello@matteocollina.com> * fixup Signed-off-by: Matteo Collina <hello@matteocollina.com> * lint * fixup Signed-off-by: Matteo Collina <hello@matteocollina.com> --------- Signed-off-by: Matteo Collina <hello@matteocollina.com> Signed-off-by: Matteo Collina <matteo.collina@gmail.com> Co-authored-by: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent d771fdf commit da7ce60

File tree

6 files changed

+152
-0
lines changed

6 files changed

+152
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ fastify.post('/upload/files', async function (req, reply) {
235235
const body = Object.fromEntries(
236236
Object.keys(req.body).map((key) => [key, req.body[key].value])
237237
) // Request body in key-value pairs, like req.body in Express (Node 12+)
238+
239+
// On Node 18+
240+
const formData = await req.formData()
241+
console.log(formData)
238242
})
239243
```
240244

@@ -518,6 +522,7 @@ fastify.post('/upload/files', async function (req, reply) {
518522
This project is kindly sponsored by:
519523
- [nearForm](https://nearform.com)
520524
- [LetzDoIt](https://www.letzdoitapp.com/)
525+
- [platformatic](https://platformatic.dev)
521526

522527
## License
523528

index.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const PrototypeViolationError = createError('FST_PROTO_VIOLATION', 'prototype pr
2626
const InvalidMultipartContentTypeError = createError('FST_INVALID_MULTIPART_CONTENT_TYPE', 'the request is not multipart', 406)
2727
const InvalidJSONFieldError = createError('FST_INVALID_JSON_FIELD_ERROR', 'a request field is not a valid JSON as declared by its Content-Type', 406)
2828
const FileBufferNotFoundError = createError('FST_FILE_BUFFER_NOT_FOUND', 'the file buffer was not found', 500)
29+
const NoFormData = createError('FST_NO_FORM_DATA', 'FormData is not available', 500)
2930

3031
function setMultipart (req, payload, done) {
3132
req[kMultipart] = true
@@ -123,6 +124,47 @@ function fastifyMultipart (fastify, options, done) {
123124
req.body = body
124125
}
125126
})
127+
128+
// The following is not available on old Node.js versions
129+
// so we must skip it in the test coverage
130+
/* istanbul ignore next */
131+
if (globalThis.FormData && !fastify.hasRequestDecorator('formData')) {
132+
fastify.decorateRequest('formData', async function () {
133+
const formData = new FormData()
134+
for (const key in this.body) {
135+
const value = this.body[key]
136+
if (Array.isArray(value)) {
137+
for (const item of value) {
138+
await append(key, item)
139+
}
140+
} else {
141+
await append(key, value)
142+
}
143+
}
144+
145+
async function append (key, entry) {
146+
if (entry.type === 'file' || (attachFieldsToBody === 'keyValues' && Buffer.isBuffer(entry))) {
147+
// TODO use File constructor with fs.openAsBlob()
148+
// if attachFieldsToBody is not set
149+
// https://nodejs.org/api/fs.html#fsopenasblobpath-options
150+
formData.append(key, new Blob([await entry.toBuffer()], {
151+
type: entry.mimetype
152+
}), entry.filename)
153+
} else {
154+
formData.append(key, entry.value)
155+
}
156+
}
157+
158+
return formData
159+
})
160+
}
161+
}
162+
163+
/* istanbul ignore next */
164+
if (!fastify.hasRequestDecorator('formData')) {
165+
fastify.decorateRequest('formData', async function () {
166+
throw new NoFormData()
167+
})
126168
}
127169

128170
const defaultThrowFileSizeLimit = typeof options.throwFileSizeLimit === 'boolean'

test/multipart-attach-body.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,3 +486,58 @@ test('should pass the buffer instead of converting to string', async function (t
486486
await once(res, 'end')
487487
t.pass('res ended successfully')
488488
})
489+
490+
const hasGlobalFormData = typeof globalThis.FormData === 'function'
491+
492+
test('should be able to attach all parsed fields and files and make it accessible through "req.formdata"', { skip: !hasGlobalFormData }, async function (t) {
493+
t.plan(10)
494+
495+
const fastify = Fastify()
496+
t.teardown(fastify.close.bind(fastify))
497+
498+
fastify.register(multipart, { attachFieldsToBody: true })
499+
500+
const original = fs.readFileSync(filePath, 'utf8')
501+
502+
fastify.post('/', async function (req, reply) {
503+
t.ok(req.isMultipart())
504+
505+
t.same(Object.keys(req.body), ['upload', 'hello'])
506+
507+
const formData = await req.formData()
508+
509+
t.equal(formData instanceof globalThis.FormData, true)
510+
t.equal(formData.get('hello'), 'world')
511+
t.same(formData.getAll('hello'), ['world', 'foo'])
512+
t.equal(await formData.get('upload').text(), original)
513+
t.equal(formData.get('upload').type, 'text/markdown')
514+
t.equal(formData.get('upload').name, 'README.md')
515+
516+
reply.code(200).send()
517+
})
518+
519+
await fastify.listen({ port: 0 })
520+
521+
// request
522+
const form = new FormData()
523+
const opts = {
524+
protocol: 'http:',
525+
hostname: 'localhost',
526+
port: fastify.server.address().port,
527+
path: '/',
528+
headers: form.getHeaders(),
529+
method: 'POST'
530+
}
531+
532+
const req = http.request(opts)
533+
form.append('upload', fs.createReadStream(filePath))
534+
form.append('hello', 'world')
535+
form.append('hello', 'foo')
536+
form.pipe(req)
537+
538+
const [res] = await once(req, 'response')
539+
t.equal(res.statusCode, 200)
540+
res.resume()
541+
await once(res, 'end')
542+
t.pass('res ended successfully')
543+
})

test/multipart.test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,3 +689,50 @@ test('should not freeze when error is thrown during processing', { skip: process
689689

690690
await app.close()
691691
})
692+
693+
const hasGlobalFormData = typeof globalThis.FormData === 'function'
694+
695+
test('no formData', { skip: !hasGlobalFormData }, function (t) {
696+
t.plan(6)
697+
const fastify = Fastify()
698+
t.teardown(fastify.close.bind(fastify))
699+
700+
fastify.register(multipart)
701+
702+
fastify.post('/', async function (req, reply) {
703+
await t.rejects(req.formData())
704+
705+
for await (const part of req.parts()) {
706+
t.equal(part.type, 'field')
707+
t.equal(part.fieldname, 'hello')
708+
t.equal(part.value, 'world')
709+
}
710+
711+
reply.code(200).send()
712+
})
713+
714+
fastify.listen({ port: 0 }, async function () {
715+
// request
716+
const form = new FormData()
717+
const opts = {
718+
protocol: 'http:',
719+
hostname: 'localhost',
720+
port: fastify.server.address().port,
721+
path: '/',
722+
headers: form.getHeaders(),
723+
method: 'POST'
724+
}
725+
726+
const req = http.request(opts, (res) => {
727+
t.equal(res.statusCode, 200)
728+
// consume all data without processing
729+
res.resume()
730+
res.on('end', () => {
731+
t.pass('res ended successfully')
732+
})
733+
})
734+
form.append('hello', 'world')
735+
736+
form.pipe(req)
737+
})
738+
})

types/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ declare module 'fastify' {
77
interface FastifyRequest {
88
isMultipart: () => boolean;
99

10+
formData: () => Promise<FormData>;
11+
1012
// promise api
1113
parts: (
1214
options?: Omit<BusboyConfig, 'headers'>

types/index.test-d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const runServer = async () => {
2424

2525
// usage
2626
app.post('/', async (req, reply) => {
27+
expectType<Promise<FormData>>(req.formData())
2728
const data = await req.file()
2829
if (data == null) throw new Error('missing file')
2930

0 commit comments

Comments
 (0)