Skip to content

Commit 4c0079c

Browse files
authored
Support for saveRequestFiles with attachFieldsToBody set true (#409)
* wip: failing test added * fix: save request files from body added * chore: test assertions extended * feat: saveRequestFiles error on null buff added * chore: fixed linting * chore: fixed linting * chore: async removed from filesFromFields generator
1 parent c716093 commit 4c0079c

File tree

2 files changed

+177
-4
lines changed

2 files changed

+177
-4
lines changed

index.js

+34-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const util = require('util')
1212
const createError = require('@fastify/error')
1313
const sendToWormhole = require('stream-wormhole')
1414
const deepmergeAll = require('@fastify/deepmerge')({ all: true })
15-
const { PassThrough, pipeline } = require('stream')
15+
const { PassThrough, pipeline, Readable } = require('stream')
1616
const pump = util.promisify(pipeline)
1717
const secureJSON = require('secure-json-parse')
1818

@@ -27,6 +27,7 @@ const RequestFileTooLargeError = createError('FST_REQ_FILE_TOO_LARGE', 'request
2727
const PrototypeViolationError = createError('FST_PROTO_VIOLATION', 'prototype property is not allowed as field name', 400)
2828
const InvalidMultipartContentTypeError = createError('FST_INVALID_MULTIPART_CONTENT_TYPE', 'the request is not multipart', 406)
2929
const InvalidJSONFieldError = createError('FST_INVALID_JSON_FIELD_ERROR', 'a request field is not a valid JSON as declared by its Content-Type', 406)
30+
const FileBufferNotFoundError = createError('FST_FILE_BUFFER_NOT_FOUND', 'the file buffer was not found', 500)
3031

3132
function setMultipart (req, payload, done) {
3233
// nothing to do, it will be done by the Request.multipart object
@@ -109,6 +110,7 @@ function busboy (options) {
109110
}
110111

111112
function fastifyMultipart (fastify, options, done) {
113+
const attachFieldsToBody = options.attachFieldsToBody
112114
if (options.addToBody === true) {
113115
if (typeof options.sharedSchemaId === 'string') {
114116
fastify.addSchema({
@@ -187,7 +189,8 @@ function fastifyMultipart (fastify, options, done) {
187189
FieldsLimitError,
188190
PrototypeViolationError,
189191
InvalidMultipartContentTypeError,
190-
RequestFileTooLargeError
192+
RequestFileTooLargeError,
193+
FileBufferNotFoundError
191194
})
192195

193196
fastify.addContentTypeParser('multipart/form-data', setMultipart)
@@ -507,10 +510,14 @@ function fastifyMultipart (fastify, options, done) {
507510
}
508511

509512
async function saveRequestFiles (options) {
513+
let files
514+
if (attachFieldsToBody === true) {
515+
files = filesFromFields.call(this, this.body)
516+
} else {
517+
files = await this.files(options)
518+
}
510519
const requestFiles = []
511520
const tmpdir = (options && options.tmpdir) || os.tmpdir()
512-
513-
const files = await this.files(options)
514521
this.tmpUploads = []
515522
for await (const file of files) {
516523
const filepath = path.join(tmpdir, toID() + path.extname(file.filename))
@@ -528,6 +535,29 @@ function fastifyMultipart (fastify, options, done) {
528535
return requestFiles
529536
}
530537

538+
function * filesFromFields (container) {
539+
try {
540+
for (const field of Object.values(container)) {
541+
if (Array.isArray(field)) {
542+
for (const subField of filesFromFields.call(this, field)) {
543+
yield subField
544+
}
545+
}
546+
if (!field.file) {
547+
continue
548+
}
549+
if (!field._buf) {
550+
throw new FileBufferNotFoundError()
551+
}
552+
field.file = Readable.from(field._buf)
553+
yield field
554+
}
555+
} catch (err) {
556+
this.log.error({ err }, 'save request file failed')
557+
throw err
558+
}
559+
}
560+
531561
async function cleanRequestFiles () {
532562
if (!this.tmpUploads) {
533563
return

test/fix-313.test.js

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
'use strict'
2+
3+
const test = require('tap').test
4+
const FormData = require('form-data')
5+
const Fastify = require('fastify')
6+
const multipart = require('..')
7+
const http = require('http')
8+
const path = require('path')
9+
const fs = require('fs')
10+
const { access } = require('fs').promises
11+
const EventEmitter = require('events')
12+
const { once } = EventEmitter
13+
14+
const filePath = path.join(__dirname, '../README.md')
15+
16+
test('should store file on disk, remove on response when attach fields to body is true', async function (t) {
17+
t.plan(22)
18+
19+
const fastify = Fastify()
20+
t.teardown(fastify.close.bind(fastify))
21+
22+
fastify.register(multipart, {
23+
attachFieldsToBody: true
24+
})
25+
26+
fastify.post('/', async function (req, reply) {
27+
t.ok(req.isMultipart())
28+
29+
const files = await req.saveRequestFiles()
30+
31+
t.ok(files[0].filepath)
32+
t.equal(files[0].fieldname, 'upload')
33+
t.equal(files[0].filename, 'README.md')
34+
t.equal(files[0].encoding, '7bit')
35+
t.equal(files[0].mimetype, 'text/markdown')
36+
t.ok(files[0].fields.upload)
37+
t.ok(files[1].filepath)
38+
t.equal(files[1].fieldname, 'upload')
39+
t.equal(files[1].filename, 'README.md')
40+
t.equal(files[1].encoding, '7bit')
41+
t.equal(files[1].mimetype, 'text/markdown')
42+
t.ok(files[1].fields.upload)
43+
t.ok(files[2].filepath)
44+
t.equal(files[2].fieldname, 'other')
45+
t.equal(files[2].filename, 'README.md')
46+
t.equal(files[2].encoding, '7bit')
47+
t.equal(files[2].mimetype, 'text/markdown')
48+
t.ok(files[2].fields.upload)
49+
50+
await access(files[0].filepath, fs.constants.F_OK)
51+
await access(files[1].filepath, fs.constants.F_OK)
52+
await access(files[2].filepath, fs.constants.F_OK)
53+
54+
reply.code(200).send()
55+
})
56+
const ee = new EventEmitter()
57+
58+
// ensure that file is removed after response
59+
fastify.addHook('onResponse', async (request, reply) => {
60+
try {
61+
await access(request.tmpUploads[0], fs.constants.F_OK)
62+
} catch (error) {
63+
t.equal(error.code, 'ENOENT')
64+
t.pass('Temp file was removed after response')
65+
ee.emit('response')
66+
}
67+
})
68+
69+
await fastify.listen({ port: 0 })
70+
// request
71+
const form = new FormData()
72+
const opts = {
73+
protocol: 'http:',
74+
hostname: 'localhost',
75+
port: fastify.server.address().port,
76+
path: '/',
77+
headers: form.getHeaders(),
78+
method: 'POST'
79+
}
80+
81+
const req = http.request(opts)
82+
form.append('upload', fs.createReadStream(filePath))
83+
form.append('upload', fs.createReadStream(filePath))
84+
form.append('other', fs.createReadStream(filePath))
85+
86+
form.pipe(req)
87+
88+
const [res] = await once(req, 'response')
89+
t.equal(res.statusCode, 200)
90+
res.resume()
91+
await once(res, 'end')
92+
await once(ee, 'response')
93+
})
94+
95+
test('should throw on saving request files when attach fields to body is true but buffer is not stored', async function (t) {
96+
t.plan(3)
97+
98+
const fastify = Fastify()
99+
t.teardown(fastify.close.bind(fastify))
100+
101+
fastify.register(multipart, {
102+
attachFieldsToBody: true,
103+
onFile: async (part) => {
104+
for await (const chunk of part.file) {
105+
chunk.toString()
106+
}
107+
}
108+
})
109+
110+
fastify.post('/', async function (req, reply) {
111+
t.ok(req.isMultipart())
112+
113+
try {
114+
await req.saveRequestFiles()
115+
reply.code(200).send()
116+
} catch (error) {
117+
t.ok(error instanceof fastify.multipartErrors.FileBufferNotFoundError)
118+
reply.code(500).send()
119+
}
120+
})
121+
122+
await fastify.listen({ port: 0 })
123+
// request
124+
const form = new FormData()
125+
const opts = {
126+
protocol: 'http:',
127+
hostname: 'localhost',
128+
port: fastify.server.address().port,
129+
path: '/',
130+
headers: form.getHeaders(),
131+
method: 'POST'
132+
}
133+
134+
const req = http.request(opts)
135+
form.append('upload', fs.createReadStream(filePath))
136+
137+
form.pipe(req)
138+
139+
const [res] = await once(req, 'response')
140+
t.equal(res.statusCode, 500)
141+
res.resume()
142+
await once(res, 'end')
143+
})

0 commit comments

Comments
 (0)