Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FormData decorator to request #522

Merged
merged 9 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
41 changes: 41 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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,46 @@
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.decorateRequest('formData', async function () {
mcollina marked this conversation as resolved.
Show resolved Hide resolved
mcollina marked this conversation as resolved.
Show resolved Hide resolved
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) {
kibertoad marked this conversation as resolved.
Show resolved Hide resolved
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)
mcollina marked this conversation as resolved.
Show resolved Hide resolved
} else {
formData.append(key, entry.value)
}
}

return formData
})
}
}

Check failure on line 161 in index.js

View workflow job for this annotation

GitHub Actions / test / Lint Code

Trailing spaces not allowed

if (!fastify.hasRequestDecorator('formData')) {
fastify.decorateRequest('formData', async function () {
throw new NoFormData()
mcollina marked this conversation as resolved.
Show resolved Hide resolved
})
}

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
Loading