Skip to content

Commit 12354e5

Browse files
authored
feat: return values from saveRequestFiles (#612)
1 parent 0b7f473 commit 12354e5

File tree

9 files changed

+154
-51
lines changed

9 files changed

+154
-51
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,16 @@ This will store all files in the operating system's default directory for tempor
186186

187187
```js
188188
fastify.post('/upload/files', async function (req, reply) {
189-
// stores files to tmp dir and return files
190-
const files = await req.saveRequestFiles()
189+
// stores files to tmp dir and returns files + parsed values
190+
const { files, values } = await req.saveRequestFiles()
191191
files[0].type // "file"
192192
files[0].filepath
193193
files[0].fieldname
194194
files[0].filename
195195
files[0].encoding
196196
files[0].mimetype
197197
files[0].fields // other parsed parts
198+
values.hello?.value // non-file fields are available even if no file was uploaded
198199

199200
reply.send()
200201
})

index.js

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const secureJSON = require('secure-json-parse')
1616

1717
const kMultipart = Symbol('multipart')
1818
const kMultipartHandler = Symbol('multipartHandler')
19+
const kSavedRequestFilesResult = Symbol('savedRequestFilesResult')
1920

2021
const PartsLimitError = createError('FST_PARTS_LIMIT', 'reach parts limit', 413)
2122
const FilesLimitError = createError('FST_FILES_LIMIT', 'reach files limit', 413)
@@ -184,6 +185,7 @@ function fastifyMultipart (fastify, options, done) {
184185
fastify.addContentTypeParser('multipart/form-data', setMultipart)
185186
fastify.decorateRequest(kMultipart, false)
186187
fastify.decorateRequest(kMultipartHandler, handleMultipart)
188+
fastify.decorateRequest(kSavedRequestFilesResult, null)
187189

188190
fastify.decorateRequest('parts', getMultipartIterator)
189191

@@ -450,37 +452,51 @@ function fastifyMultipart (fastify, options, done) {
450452

451453
async function saveRequestFiles (options) {
452454
// Checks if this has already been run
453-
if (this.savedRequestFiles) {
454-
return this.savedRequestFiles
455+
if (this[kSavedRequestFilesResult]) {
456+
return this[kSavedRequestFilesResult]
455457
}
456-
let files
457-
if (attachFieldsToBody === true) {
458-
// Skip the whole process if the body is empty
459-
if (!this.body) {
460-
return []
461-
}
462-
files = filesFromFields.call(this, this.body)
458+
459+
let parts
460+
let values = {}
461+
462+
if (attachFieldsToBody === true || attachFieldsToBody === 'keyValues') {
463+
parts = this.body ? filesFromFields.call(this, this.body) : []
464+
values = this.body || {}
463465
} else {
464-
files = await this.files(options)
466+
parts = this.parts(options)
465467
}
468+
466469
this.savedRequestFiles = []
467470
const tmpdir = options?.tmpdir || os.tmpdir()
468471
this.tmpUploads = []
469472
let i = 0
470-
for await (const file of files) {
471-
const filepath = path.join(tmpdir, generateId() + path.extname(file.filename || ('file' + i++)))
473+
for await (const part of parts) {
474+
values = part.fields
475+
476+
if (!part.file) {
477+
continue
478+
}
479+
480+
const filepath = path.join(tmpdir, generateId() + path.extname(part.filename || ('file' + i++)))
472481
const target = createWriteStream(filepath)
473482
try {
474483
this.tmpUploads.push(filepath)
475-
await pump(file.file, target)
476-
this.savedRequestFiles.push({ ...file, filepath })
484+
await pump(part.file, target)
485+
this.savedRequestFiles.push({ ...part, filepath })
477486
} catch (err) {
487+
target.destroy()
488+
await this.cleanRequestFiles()
478489
this.log.error({ err }, 'save request file')
479490
throw err
480491
}
481492
}
482493

483-
return this.savedRequestFiles
494+
this[kSavedRequestFilesResult] = {
495+
files: this.savedRequestFiles,
496+
values
497+
}
498+
499+
return this[kSavedRequestFilesResult]
484500
}
485501

486502
function * filesFromFields (container) {

test/fix-313.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ test('should store file on disk, remove on response when attach fields to body i
2626
fastify.post('/', async function (req, reply) {
2727
t.assert.ok(req.isMultipart())
2828

29-
const files = await req.saveRequestFiles()
29+
const { files } = await req.saveRequestFiles()
3030

3131
t.assert.ok(files[0].filepath)
3232
t.assert.strictEqual(files[0].type, 'file')

test/multipart-ajv-file.test.js

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const test = require('node:test')
1212
const filePath = path.join(__dirname, '../README.md')
1313

1414
test('show modify the generated schema', async t => {
15-
t.plan(4)
15+
t.plan(6)
1616

1717
const fastify = Fastify({
1818
ajv: {
@@ -52,29 +52,16 @@ test('show modify the generated schema', async t => {
5252

5353
await fastify.ready()
5454

55-
t.assert.deepStrictEqual(fastify.swagger().paths, {
56-
'/': {
57-
post: {
58-
operationId: 'test',
59-
requestBody: {
60-
content: {
61-
'multipart/form-data': {
62-
schema: {
63-
type: 'object',
64-
properties: {
65-
field: { type: 'string', format: 'binary' }
66-
}
67-
}
68-
}
69-
},
70-
required: true,
71-
},
72-
responses: {
73-
200: { description: 'Default Response' }
74-
}
75-
}
55+
const { post } = fastify.swagger().paths['/']
56+
57+
t.assert.strictEqual(post.operationId, 'test')
58+
t.assert.deepStrictEqual(post.requestBody.content['multipart/form-data'].schema, {
59+
type: 'object',
60+
properties: {
61+
field: { type: 'string', format: 'binary' }
7662
}
7763
})
64+
t.assert.strictEqual(post.responses[200].description, 'Default Response')
7865

7966
await fastify.listen({ port: 0 })
8067

test/multipart-disk.test.js

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ test('should store file on disk, remove on response', async function (t) {
2727
fastify.post('/', async function (req, reply) {
2828
t.assert.ok(req.isMultipart())
2929

30-
const files = await req.saveRequestFiles()
30+
const { files } = await req.saveRequestFiles()
3131

3232
t.assert.ok(files[0].filepath)
3333
t.assert.strictEqual(files[0].fieldname, 'upload')
@@ -135,6 +135,96 @@ test('should store file on disk, remove on response error', async function (t) {
135135
await once(ee, 'response')
136136
})
137137

138+
test('should return saved files and values from saveRequestFiles', async function (t) {
139+
t.plan(8)
140+
141+
const fastify = Fastify()
142+
t.after(() => fastify.close())
143+
144+
fastify.register(multipart)
145+
146+
fastify.post('/', async function (req, reply) {
147+
const { files, values } = await req.saveRequestFiles()
148+
149+
t.assert.ok(Array.isArray(files))
150+
t.assert.strictEqual(files.length, 1)
151+
t.assert.strictEqual(files[0].fieldname, 'upload')
152+
t.assert.strictEqual(values.hello.value, 'world')
153+
t.assert.strictEqual(values.count.value, '42')
154+
t.assert.ok(values.upload)
155+
156+
reply.code(200).send()
157+
})
158+
159+
await fastify.listen({ port: 0 })
160+
161+
const form = new FormData()
162+
const opts = {
163+
protocol: 'http:',
164+
hostname: 'localhost',
165+
port: fastify.server.address().port,
166+
path: '/',
167+
headers: form.getHeaders(),
168+
method: 'POST'
169+
}
170+
171+
const req = http.request(opts)
172+
form.append('hello', 'world')
173+
form.append('count', '42')
174+
form.append('upload', fs.createReadStream(filePath))
175+
176+
form.pipe(req)
177+
178+
const [res] = await once(req, 'response')
179+
t.assert.strictEqual(res.statusCode, 200)
180+
res.resume()
181+
await once(res, 'end')
182+
t.assert.ok('res ended successfully')
183+
})
184+
185+
test('should return values when saveRequestFiles receives only fields', async function (t) {
186+
t.plan(6)
187+
188+
const fastify = Fastify()
189+
t.after(() => fastify.close())
190+
191+
fastify.register(multipart)
192+
193+
fastify.post('/', async function (req, reply) {
194+
const { files, values } = await req.saveRequestFiles()
195+
196+
t.assert.ok(Array.isArray(files))
197+
t.assert.strictEqual(files.length, 0)
198+
t.assert.strictEqual(values.hello.value, 'world')
199+
t.assert.strictEqual(values.count.value, '42')
200+
201+
reply.code(200).send()
202+
})
203+
204+
await fastify.listen({ port: 0 })
205+
206+
const form = new FormData()
207+
const opts = {
208+
protocol: 'http:',
209+
hostname: 'localhost',
210+
port: fastify.server.address().port,
211+
path: '/',
212+
headers: form.getHeaders(),
213+
method: 'POST'
214+
}
215+
216+
const req = http.request(opts)
217+
form.append('hello', 'world')
218+
form.append('count', '42')
219+
form.pipe(req)
220+
221+
const [res] = await once(req, 'response')
222+
t.assert.strictEqual(res.statusCode, 200)
223+
res.resume()
224+
await once(res, 'end')
225+
t.assert.ok('res ended successfully')
226+
})
227+
138228
test('should throw on file limit error', async function (t) {
139229
t.plan(4)
140230

test/multipart-duplicate-save-request-file.test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const { once } = EventEmitter
1313
const filePath = path.join(__dirname, '../README.md')
1414

1515
test('should store file on disk, remove on response', async function (t) {
16-
t.plan(3)
16+
t.plan(4)
1717

1818
const fastify = Fastify()
1919
t.after(() => fastify.close())
@@ -23,11 +23,12 @@ test('should store file on disk, remove on response', async function (t) {
2323
await fastify.post('/', async function (req, reply) {
2424
t.assert.ok(req.isMultipart())
2525

26-
const files = await req.saveRequestFiles()
27-
const files2 = await req.saveRequestFiles()
26+
const { files, values } = await req.saveRequestFiles()
27+
const { files: files2, values: values2 } = await req.saveRequestFiles()
2828

2929
// If it really reused the previously response, their filepath should be the same
3030
t.assert.strictEqual(files[0].filepath, files2[0].filepath)
31+
t.assert.strictEqual(values, values2)
3132

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

test/multipart-empty-body.test.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const http = require('node:http')
88
const { once } = require('node:events')
99

1010
test('should not break with a empty request body when attachFieldsToBody is true', async function (t) {
11-
t.plan(5)
11+
t.plan(6)
1212

1313
const fastify = Fastify()
1414
t.after(() => fastify.close())
@@ -18,10 +18,11 @@ test('should not break with a empty request body when attachFieldsToBody is true
1818
fastify.post('/', async function (req, reply) {
1919
t.assert.ok(req.isMultipart())
2020

21-
const files = await req.saveRequestFiles()
21+
const { files, values } = await req.saveRequestFiles()
2222

2323
t.assert.ok(Array.isArray(files))
2424
t.assert.strictEqual(files.length, 0)
25+
t.assert.deepStrictEqual(values, {})
2526

2627
reply.code(200).send()
2728
})
@@ -50,7 +51,7 @@ test('should not break with a empty request body when attachFieldsToBody is true
5051
})
5152

5253
test('should not break with a empty request body when attachFieldsToBody is keyValues', async function (t) {
53-
t.plan(5)
54+
t.plan(6)
5455

5556
const fastify = Fastify()
5657
t.after(() => fastify.close())
@@ -60,10 +61,11 @@ test('should not break with a empty request body when attachFieldsToBody is keyV
6061
fastify.post('/', async function (req, reply) {
6162
t.assert.ok(req.isMultipart())
6263

63-
const files = await req.saveRequestFiles()
64+
const { files, values } = await req.saveRequestFiles()
6465

6566
t.assert.ok(Array.isArray(files))
6667
t.assert.strictEqual(files.length, 0)
68+
t.assert.deepStrictEqual(values, {})
6769

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

types/index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ declare module 'fastify' {
2525
// Disk mode
2626
saveRequestFiles: (
2727
options?: Omit<BusboyConfig, 'headers'> & { tmpdir?: string }
28-
) => Promise<Array<fastifyMultipart.SavedMultipartFile>>;
28+
) => Promise<fastifyMultipart.SavedMultipartFilesResult>;
2929
cleanRequestFiles: () => Promise<void>;
3030
tmpUploads: Array<string> | null;
3131
/** This will get populated as soon as a call to `saveRequestFiles` gets resolved. Avoiding any future duplicate work */
@@ -72,6 +72,11 @@ declare namespace fastifyMultipart {
7272
filepath: string;
7373
}
7474

75+
export interface SavedMultipartFilesResult {
76+
files: Array<SavedMultipartFile>;
77+
values: MultipartFields | Record<string, unknown>;
78+
}
79+
7580
export type Multipart = MultipartFile | MultipartValue
7681

7782
export interface MultipartFile {

types/index.test-d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,16 @@ const runServer = async () => {
138138

139139
// upload files to disk and work with temporary file paths
140140
app.post('/upload/files', async function (req, reply) {
141-
// stores files to tmp dir and return files
142-
const files = await req.saveRequestFiles()
141+
// stores files to tmp dir and return files + values
142+
const { files, values } = await req.saveRequestFiles()
143143
files[0].type // "file"
144144
files[0].filepath
145145
files[0].fieldname
146146
files[0].filename
147147
files[0].encoding
148148
files[0].mimetype
149149
files[0].fields // other parsed parts
150+
values.foo
150151

151152
reply.send()
152153
})

0 commit comments

Comments
 (0)