Skip to content

Commit

Permalink
Add FormData decorator to request (#522)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
mcollina and gurgunday committed May 10, 2024
1 parent d771fdf commit da7ce60
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@ fastify.post('/upload/files', async function (req, reply) {
const body = Object.fromEntries(
Object.keys(req.body).map((key) => [key, req.body[key].value])
) // Request body in key-value pairs, like req.body in Express (Node 12+)

// On Node 18+
const formData = await req.formData()
console.log(formData)
})
```

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

## License

Expand Down
42 changes: 42 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const PrototypeViolationError = createError('FST_PROTO_VIOLATION', 'prototype pr
const InvalidMultipartContentTypeError = createError('FST_INVALID_MULTIPART_CONTENT_TYPE', 'the request is not multipart', 406)
const InvalidJSONFieldError = createError('FST_INVALID_JSON_FIELD_ERROR', 'a request field is not a valid JSON as declared by its Content-Type', 406)
const FileBufferNotFoundError = createError('FST_FILE_BUFFER_NOT_FOUND', 'the file buffer was not found', 500)
const NoFormData = createError('FST_NO_FORM_DATA', 'FormData is not available', 500)

function setMultipart (req, payload, done) {
req[kMultipart] = true
Expand Down Expand Up @@ -123,6 +124,47 @@ function fastifyMultipart (fastify, options, done) {
req.body = body
}
})

// The following is not available on old Node.js versions
// so we must skip it in the test coverage
/* istanbul ignore next */
if (globalThis.FormData && !fastify.hasRequestDecorator('formData')) {
fastify.decorateRequest('formData', async function () {
const formData = new FormData()
for (const key in this.body) {
const value = this.body[key]
if (Array.isArray(value)) {
for (const item of value) {
await append(key, item)
}
} else {
await append(key, value)
}
}

async function append (key, entry) {
if (entry.type === 'file' || (attachFieldsToBody === 'keyValues' && Buffer.isBuffer(entry))) {
// TODO use File constructor with fs.openAsBlob()
// if attachFieldsToBody is not set
// https://nodejs.org/api/fs.html#fsopenasblobpath-options
formData.append(key, new Blob([await entry.toBuffer()], {
type: entry.mimetype
}), entry.filename)
} else {
formData.append(key, entry.value)
}
}

return formData
})
}
}

/* istanbul ignore next */
if (!fastify.hasRequestDecorator('formData')) {
fastify.decorateRequest('formData', async function () {
throw new NoFormData()
})
}

const defaultThrowFileSizeLimit = typeof options.throwFileSizeLimit === 'boolean'
Expand Down
55 changes: 55 additions & 0 deletions test/multipart-attach-body.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,58 @@ test('should pass the buffer instead of converting to string', async function (t
await once(res, 'end')
t.pass('res ended successfully')
})

const hasGlobalFormData = typeof globalThis.FormData === 'function'

test('should be able to attach all parsed fields and files and make it accessible through "req.formdata"', { skip: !hasGlobalFormData }, async function (t) {
t.plan(10)

const fastify = Fastify()
t.teardown(fastify.close.bind(fastify))

fastify.register(multipart, { attachFieldsToBody: true })

const original = fs.readFileSync(filePath, 'utf8')

fastify.post('/', async function (req, reply) {
t.ok(req.isMultipart())

t.same(Object.keys(req.body), ['upload', 'hello'])

const formData = await req.formData()

t.equal(formData instanceof globalThis.FormData, true)
t.equal(formData.get('hello'), 'world')
t.same(formData.getAll('hello'), ['world', 'foo'])
t.equal(await formData.get('upload').text(), original)
t.equal(formData.get('upload').type, 'text/markdown')
t.equal(formData.get('upload').name, 'README.md')

reply.code(200).send()
})

await fastify.listen({ port: 0 })

// request
const form = new FormData()
const opts = {
protocol: 'http:',
hostname: 'localhost',
port: fastify.server.address().port,
path: '/',
headers: form.getHeaders(),
method: 'POST'
}

const req = http.request(opts)
form.append('upload', fs.createReadStream(filePath))
form.append('hello', 'world')
form.append('hello', 'foo')
form.pipe(req)

const [res] = await once(req, 'response')
t.equal(res.statusCode, 200)
res.resume()
await once(res, 'end')
t.pass('res ended successfully')
})
47 changes: 47 additions & 0 deletions test/multipart.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -689,3 +689,50 @@ test('should not freeze when error is thrown during processing', { skip: process

await app.close()
})

const hasGlobalFormData = typeof globalThis.FormData === 'function'

test('no formData', { skip: !hasGlobalFormData }, function (t) {
t.plan(6)
const fastify = Fastify()
t.teardown(fastify.close.bind(fastify))

fastify.register(multipart)

fastify.post('/', async function (req, reply) {
await t.rejects(req.formData())

for await (const part of req.parts()) {
t.equal(part.type, 'field')
t.equal(part.fieldname, 'hello')
t.equal(part.value, 'world')
}

reply.code(200).send()
})

fastify.listen({ port: 0 }, async function () {
// request
const form = new FormData()
const opts = {
protocol: 'http:',
hostname: 'localhost',
port: fastify.server.address().port,
path: '/',
headers: form.getHeaders(),
method: 'POST'
}

const req = http.request(opts, (res) => {
t.equal(res.statusCode, 200)
// consume all data without processing
res.resume()
res.on('end', () => {
t.pass('res ended successfully')
})
})
form.append('hello', 'world')

form.pipe(req)
})
})
2 changes: 2 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ declare module 'fastify' {
interface FastifyRequest {
isMultipart: () => boolean;

formData: () => Promise<FormData>;

// promise api
parts: (
options?: Omit<BusboyConfig, 'headers'>
Expand Down
1 change: 1 addition & 0 deletions types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const runServer = async () => {

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

Expand Down

0 comments on commit da7ce60

Please sign in to comment.