Skip to content

Commit

Permalink
Option to attach to body as key-value pairs (#348)
Browse files Browse the repository at this point in the history
Co-authored-by: Igor Savin <iselwin@gmail.com>
  • Loading branch information
zone117x and kibertoad committed Jun 8, 2022
1 parent c04dcb7 commit 7aa5b75
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 3 deletions.
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,17 @@ fastify.post('/upload/files', async function (req, reply) {
})
```

Request body key-value pairs can be assigned directly using `attachFieldsToBody: 'keyValues'`. Field values will be attached directly to the body object. By default, all files are converted to a string using `buffer.toString()` used as the value attached to the body.

```js
fastify.register(require('@fastify/multipart'), { attachFieldsToBody: 'keyValues' })

fastify.post('/upload/files', async function (req, reply) {
const uploadValue = req.body.upload // access file as string
const fooValue = req.body.foo // other fields
})
```

You can also define an `onFile` handler to avoid accumulating all files in memory.

```js
Expand All @@ -250,13 +261,60 @@ fastify.post('/upload/files', async function (req, reply) {
})
```

The `onFile` handler can also be used with `attachFieldsToBody: 'keyValues'` in order to specify how file buffer values are decoded.

```js
async function onFile(part) {
const buff = await part.toBuffer()
const decoded = Buffer.from(buff.toString(), 'base64').toString()
part.value = decoded // set `part.value` to specify the request body value
}

fastify.register(require('@fastify/multipart'), { attachFieldsToBody: 'keyValues', onFile })

fastify.post('/upload/files', async function (req, reply) {
const uploadValue = req.body.upload // access file as base64 string
const fooValue = req.body.foo // other fields
})
```

**Note**: if you assign all fields to the body and don't define an `onFile` handler, you won't be able to read the files through streams, as they are already read and their contents are accumulated in memory.
You can only use the `toBuffer` method to read the content.
If you try to read from a stream and pipe to a new file, you will obtain an empty new file.

## JSON Schema body validation

If you enable `attachFieldsToBody` and set `sharedSchemaId` a shared JSON Schema is added, which can be used to validate parsed multipart fields.
If you enable `attachFieldsToBody: 'keyValues'` then the response body and JSON Schema validation will behave similarly to `application/json` and [`application/x-www-form-urlencoded`](https://github.com/fastify/fastify-formbody) content types. Files will be decoded using `Buffer.toString()` and attached as a body value.

```js
fastify.register(require('@fastify/multipart'), { attachFieldsToBody: 'keyValues' })

fastify.post('/upload/files', {
schema: {
body: {
type: 'object',
required: ['myFile'],
properties: {
// file that gets decoded to string
myFile: {
type: 'string',
// validate that file contents match a UUID
pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
},
hello: {
type: 'string',
enum: ['world']
}
}
}
}
}, function (req, reply) {
console.log({ body: req.body })
reply.send('done')
})
```

If you enable `attachFieldsToBody: true` and set `sharedSchemaId` a shared JSON Schema is added, which can be used to validate parsed multipart fields.

```js
const opts = {
Expand Down
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export interface FastifyMultipartAttactFieldsToBodyOptions extends FastifyMultip
/**
* Only valid in the promise api. Append the multipart parameters to the body object.
*/
attachFieldsToBody: true
attachFieldsToBody: true | 'keyValues';

/**
* Manage the file stream like you need
Expand Down
14 changes: 13 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function fastifyMultipart (fastify, options, done) {
})
}

if (options.attachFieldsToBody === true) {
if (options.attachFieldsToBody === true || options.attachFieldsToBody === 'keyValues') {
if (typeof options.sharedSchemaId === 'string') {
fastify.addSchema({
$id: options.sharedSchemaId,
Expand All @@ -147,6 +147,18 @@ function fastifyMultipart (fastify, options, done) {
}
}
}
if (options.attachFieldsToBody === 'keyValues') {
const body = {}
for (const key of Object.keys(req.body)) {
const field = req.body[key]
if (field.value !== undefined) {
body[key] = field.value
} else if (field._buf !== undefined) {
body[key] = field._buf.toString()
}
}
req.body = body
}
})
}

Expand Down
146 changes: 146 additions & 0 deletions test/multipart-attach-body.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const http = require('http')
const path = require('path')
const fs = require('fs')
const { once } = require('events')
const { Readable } = require('stream')

const filePath = path.join(__dirname, '../README.md')

Expand Down Expand Up @@ -59,6 +60,151 @@ test('should be able to attach all parsed fields and files and make it accessibl
t.pass('res ended successfully')
})

test('should be able to attach all parsed field values and json content files and make it accessible through "req.body"', async function (t) {
t.plan(6)

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

fastify.register(multipart, { attachFieldsToBody: 'keyValues' })

const original = { testContent: 'test upload content' }

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

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

t.same(req.body.upload, original)
t.equal(req.body.hello, 'world')

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', Readable.from(Buffer.from(JSON.stringify(original))), { contentType: 'application/json' })
form.append('hello', 'world')
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')
})

test('should be able to attach all parsed field values and files and make it accessible through "req.body"', async function (t) {
t.plan(6)

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

fastify.register(multipart, { attachFieldsToBody: 'keyValues' })

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

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

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

t.equal(req.body.upload, original)
t.equal(req.body.hello, 'world')

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.pipe(req)

const [res] = await once(req, 'response')
t.equal(res.statusCode, 200)
res.resume()
await once(res, 'end')
t.pass('res ended successfully')
})

test('should be able to attach all parsed field values and files with custom "onFile" handler and make it accessible through "req.body"', async function (t) {
t.plan(7)

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

async function onFile (part) {
t.pass('custom onFile handler')
const buff = await part.toBuffer()
const decoded = Buffer.from(buff.toString(), 'base64').toString()
part.value = decoded
}

fastify.register(multipart, { attachFieldsToBody: 'keyValues', onFile })

const original = 'test upload content'

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

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

t.equal(req.body.upload, original)
t.equal(req.body.hello, 'world')

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', Readable.from(Buffer.from(original).toString('base64')))
form.append('hello', 'world')
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')
})

test('should be able to define a custom "onFile" handler', async function (t) {
t.plan(7)

Expand Down

0 comments on commit 7aa5b75

Please sign in to comment.