diff --git a/.taprc b/.taprc index 7ba65e3a..4df51f34 100644 --- a/.taprc +++ b/.taprc @@ -1,4 +1,3 @@ -ts: false -jsx: false -flow: false check-coverage: false +files: + - test/**/*.test.js diff --git a/README.md b/README.md index 30874f06..6a1b2e13 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,6 @@ npm i @fastify/multipart ## Usage -If you are looking for the documentation for the legacy callback-api please see [here](./callback.md). - ```js const fastify = require('fastify')() const fs = require('node:fs') @@ -240,13 +238,13 @@ 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. +Request body key-value pairs can be assigned directly using `attachFieldsToBody: 'keyValues'`. Field values, including file buffers, will be attached to the body object. ```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 uploadValue = req.body.upload // access file as buffer const fooValue = req.body.foo // other fields }) ``` diff --git a/callback.md b/callback.md deleted file mode 100644 index 23b2b054..00000000 --- a/callback.md +++ /dev/null @@ -1,194 +0,0 @@ -# Callback-based API - -## Compatibility - -- [X] Linux -- [X] Mac -- [ ] Windows: Due to this [error](https://github.com/fastify/fastify-multipart/issues/110), we recommend to use the [new](/README.md) promise implementation. - -## Usage - -```js -const fastify = require('fastify')() -const concat = require('concat-stream') -const fs = require('node:fs') -const pump = require('pump') - -fastify.register(require('@fastify/multipart')) - -fastify.post('/', function (req, reply) { - // you can use this request's decorator to check if the request is multipart - if (!req.isMultipart()) { - reply.code(400).send(new Error('Request is not multipart')) - return - } - - const mp = req.multipart(handler, onEnd) - - // mp is an instance of - // https://www.npmjs.com/package/busboy - - mp.on('field', function (key, value) { - console.log('form-data', key, value) - }) - - function onEnd(err) { - console.log('upload completed') - reply.code(200).send() - } - - function handler (field, file, filename, encoding, mimetype) { - // to accumulate the file in memory! Be careful! - // - // file.pipe(concat(function (buf) { - // console.log('received', filename, 'size', buf.length) - // })) - // - // or - - pump(file, fs.createWriteStream('a-destination')) - - // be careful of permission issues on disk and not overwrite - // sensitive files that could cause security risks - - // also, consider that if the file stream is not consumed, the - // onEnd callback won't be called - } -}) - -fastify.listen({ port: 3000 }, err => { - if (err) throw err - console.log(`server listening on ${fastify.server.address().port}`) -}) -``` - -You can also pass optional arguments to busboy when registering with fastify. This is useful for setting limits on the content that can be uploaded. A full list of available options can be found in the [busboy documentation](https://github.com/mscdex/busboy#busboy-methods). - -**Note**: if the file stream that is provided to the handler function is not consumed (like in the example above with the usage of pump) the onEnd callback won't be called at the end of the multipart processing. -This behavior is inherited from [busboy](https://github.com/mscdex/busboy). - -```js -fastify.register(require('@fastify/multipart'), { - limits: { - fieldNameSize: 100, // Max field name size in bytes - fieldSize: 1000000, // Max field value size in bytes - fields: 10, // Max number of non-file fields - fileSize: 100, // For multipart forms, the max file size - files: 1, // Max number of file fields - headerPairs: 2000 // Max number of header key=>value pairs - } -}); -``` - -If you do set upload limits, be sure to listen for limit events in the handler method. An error or exception will not occur if a limit is reached, but rather the stream will be truncated. These events are documented in more detail [here](https://github.com/mscdex/busboy#busboy-special-events). - -```js - -mp.on('partsLimit', () => console.log('Maximum number of form parts reached')); - -mp.on('filesLimit', () => console.log('Maximum number of files reached')); - -mp.on('fieldsLimit', () => console.log('Maximim number of fields reached')); - -function handler (field, file, filename, encoding, mimetype) { - file.on('limit', () => console.log('File size limit reached')); -} -``` - -Note, if the file size limit is exceeded the file will not be attached to the body. - -Additionally, you can pass per-request options to the req.multipart function - -```js -fastify.post('/', function (req, reply) { - const options = { limits: { fileSize: 1000 } }; - const mp = req.multipart(handler, done, options) - - function done (err) { - console.log('upload completed') - reply.code(200).send() - } - - function handler (field, file, filename, encoding, mimetype) { - pump(file, fs.createWriteStream('a-destination')) - } -}) -``` - -You can also use all the parsed HTTP request parameters to the body: - -```js -const options = { - addToBody: true, - sharedSchemaId: 'MultipartFileType', // Optional shared schema id - onFile: (fieldName, stream, filename, encoding, mimetype, body) => { - // Manage the file stream like you need - // By default the data will be added in a Buffer - // Be careful to accumulate the file in memory! - // It is MANDATORY consume the stream, otherwise the response will not be processed! - // The body parameter is the object that will be added to the request - stream.resume() - }, - limit: { /*...*/ } // You can the limit options in any case -} - -fastify.register(require('@fastify/multipart'), options) - -fastify.post('/', function (req, reply) { - console.log(req.body) - // This will print out: - // { - // myStringField: 'example', - // anotherOne: 'example', - // myFilenameField: [{ - // data: , - // encoding: '7bit', - // filename: 'README.md', - // limit: false, - // mimetype: 'text/markdown' - // }] - // } - - reply.code(200).send() -}) -``` - -The options `onFile` and `sharedSchemaId` will be used only when `addToBody: true`. - -The `onFile` option define how the file streams are managed: -+ if you don't set it the `req.body.[index].data` will be a Buffer with the data loaded in memory -+ if you set it with a function you **must** consume the stream, and the `req.body.[index].data` will be an empty array - -**Note**: By default values in fields with files have array type, so if there's only one file uploaded, you can access it via `req.body.[0].data`. Regular fields become an array only when multiple values are provided. - -The `sharedSchemaId` parameter must provide a string ID and a [shared schema](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md#adding-a-shared-schema) will be added to your fastify instance so you will be able to apply the validation to your service like this: - -```js -fastify.post('/upload', { - schema: { - body: { - type: 'object', - required: ['myStringField', 'myFilenameField'], - properties: { - myStringField: { type: 'string' }, - myFilenameField: { type: 'array', items: 'MultipartFileType#' } - } - } -}, function (req, reply) { - reply.send('done') -}) -``` - -The shared schema added will be like this: - -```js -{ - type: 'object', - properties: { - encoding: { type: 'string' }, - filename: { type: 'string' }, - limit: { type: 'boolean' }, - mimetype: { type: 'string' } - } -} -``` diff --git a/examples/example-legacy-validation.js b/examples/example-legacy-validation.js deleted file mode 100644 index 40b38b16..00000000 --- a/examples/example-legacy-validation.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict' - -const fastify = require('fastify')({ logger: true }) - -const opts = { - addToBody: true, - sharedSchemaId: '#mySharedSchema' -} -fastify.register(require('..'), opts) - -fastify.post('/upload/files', { - schema: { - body: { - type: 'object', - required: ['myStringField'], - properties: { - myStringField: { type: 'string' }, - myFilenameField: { $ref: '#mySharedSchema' } - } - } - } -}, function (req, reply) { - console.log({ body: req.body }) - reply.send('done') -}) - -fastify.listen({ port: 3000 }, err => { - if (err) throw err - console.log(`server listening on ${fastify.server.address().port}`) -}) diff --git a/examples/example-legacy.js b/examples/example-legacy.js deleted file mode 100644 index d26fe6bb..00000000 --- a/examples/example-legacy.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict' - -const fastify = require('fastify')() -const fs = require('node:fs') -const path = require('node:path') -const pump = require('pump') -const form = path.join(__dirname, '..', 'form.html') - -fastify.register(require('..')) - -fastify.get('/', function (req, reply) { - reply.type('text/html').send(fs.createReadStream(form)) -}) - -fastify.post('/upload/files', function (req, reply) { - const mp = req.multipart(handler, function (err) { - if (err) { - reply.send(err) - return - } - console.log('upload completed', process.memoryUsage().rss) - reply.code(200).send() - }) - - mp.on('field', function (key, value) { - console.log('form-data', key, value) - }) - - function handler (field, file, filename, encoding, mimetype) { - pump(file, fs.createWriteStream('a-destination')) - } -}) - -fastify.listen({ port: 3000 }, err => { - if (err) throw err - console.log(`server listening on ${fastify.server.address().port}`) -}) diff --git a/index.js b/index.js index 18a22e56..ead00699 100644 --- a/index.js +++ b/index.js @@ -3,9 +3,8 @@ const Busboy = require('@fastify/busboy') const os = require('node:os') const fp = require('fastify-plugin') -const eos = require('end-of-stream') const { createWriteStream } = require('node:fs') -const { unlink } = require('node:fs').promises +const { unlink } = require('node:fs/promises') const path = require('node:path') const { generateId } = require('./lib/generateId') const util = require('node:util') @@ -30,73 +29,10 @@ const InvalidJSONFieldError = createError('FST_INVALID_JSON_FIELD_ERROR', 'a req const FileBufferNotFoundError = createError('FST_FILE_BUFFER_NOT_FOUND', 'the file buffer was not found', 500) function setMultipart (req, payload, done) { - // nothing to do, it will be done by the Request.multipart object req.raw[kMultipart] = true done() } -function attachToBody (options, req, reply, next) { - if (req.raw[kMultipart] !== true) { - next() - return - } - - const consumerStream = options.onFile || defaultConsumer - const body = {} - const mp = req.multipart((field, file, filename, encoding, mimetype) => { - body[field] = body[field] || [] - body[field].push({ - data: [], - filename, - encoding, - mimetype, - limit: false - }) - - const result = consumerStream(field, file, filename, encoding, mimetype, body) - if (result && typeof result.then === 'function') { - result.catch((err) => { - // continue with the workflow - err.statusCode = 500 - file.destroy(err) - }) - } - }, function (err) { - if (!err) { - req.body = body - } - next(err) - }, options) - - mp.on('field', (key, value) => { - if (key === '__proto__' || key === 'constructor') { - mp.destroy(new Error(`${key} is not allowed as field name`)) - return - } - if (body[key] === undefined) { - body[key] = value - } else if (Array.isArray(body[key])) { - body[key].push(value) - } else { - body[key] = [body[key], value] - } - }) -} - -function defaultConsumer (field, file, filename, encoding, mimetype, body) { - const fileData = [] - const lastFile = body[field][body[field].length - 1] - file.on('data', data => { if (!lastFile.limit) { fileData.push(data) } }) - file.on('limit', () => { lastFile.limit = true }) - file.on('end', () => { - if (!lastFile.limit) { - lastFile.data = Buffer.concat(fileData) - } else { - lastFile.data = undefined - } - }) -} - function busboy (options) { try { return new Busboy(options) @@ -117,27 +53,9 @@ function fastifyMultipart (fastify, options, done) { } const attachFieldsToBody = options.attachFieldsToBody - if (options.addToBody === true) { - if (typeof options.sharedSchemaId === 'string') { - fastify.addSchema({ - $id: options.sharedSchemaId, - type: 'object', - properties: { - encoding: { type: 'string' }, - filename: { type: 'string' }, - limit: { type: 'boolean' }, - mimetype: { type: 'string' } - } - }) - } - fastify.addHook('preValidation', function (req, reply, next) { - attachToBody(options, req, reply, next) - }) - } - - if (options.attachFieldsToBody === true || options.attachFieldsToBody === 'keyValues') { - if (typeof options.sharedSchemaId === 'string') { + if (attachFieldsToBody === true || attachFieldsToBody === 'keyValues') { + if (typeof options.sharedSchemaId === 'string' && attachFieldsToBody === true) { fastify.addSchema({ $id: options.sharedSchemaId, type: 'object', @@ -149,12 +67,15 @@ function fastifyMultipart (fastify, options, done) { } }) } + fastify.addHook('preValidation', async function (req, reply) { if (!req.isMultipart()) { return } + for await (const part of req.parts()) { req.body = part.fields + if (part.file) { if (options.onFile) { await options.onFile.call(req, part) @@ -163,25 +84,41 @@ function fastifyMultipart (fastify, options, done) { } } } - if (options.attachFieldsToBody === 'keyValues') { + + if (attachFieldsToBody === 'keyValues') { const body = {} + if (req.body) { - for (const key of Object.keys(req.body)) { + const reqBodyKeys = Object.keys(req.body) + + for (let i = 0; i < reqBodyKeys.length; ++i) { + const key = reqBodyKeys[i] const field = req.body[key] + if (field.value !== undefined) { body[key] = field.value + } else if (field._buf) { + body[key] = field._buf } else if (Array.isArray(field)) { - body[key] = field.map(item => { - if (item._buf) { - return item._buf.toString() + const items = [] + + for (let i = 0; i < field.length; ++i) { + const item = field[i] + + if (item.value !== undefined) { + items.push(item.value) + } else if (item._buf) { + items.push(item._buf) } - return item.value - }) - } else if (field._buf) { - body[key] = field._buf.toString() + } + + if (items.length) { + body[key] = items + } } } } + req.body = body } }) @@ -210,9 +147,6 @@ function fastifyMultipart (fastify, options, done) { fastify.decorateRequest('tmpUploads', null) fastify.decorateRequest('savedRequestFiles', null) - // legacy - fastify.decorateRequest('multipart', handleLegacyMultipartApi) - // Stream mode fastify.decorateRequest('file', getMultipartFile) fastify.decorateRequest('files', getMultipartFiles) @@ -226,80 +160,7 @@ function fastifyMultipart (fastify, options, done) { }) function isMultipart () { - return this.raw[kMultipart] || false - } - - // handler definition is in multipart-readstream - // handler(field, file, filename, encoding, mimetype) - // opts is a per-request override for the options object - function handleLegacyMultipartApi (handler, done, opts) { - if (typeof handler !== 'function') { - throw new Error('handler must be a function') - } - - if (typeof done !== 'function') { - throw new Error('the callback must be a function') - } - - if (!this.isMultipart()) { - done(new Error('the request is not multipart')) - return - } - - const log = this.log - - log.warn('the multipart callback-based api is deprecated in favour of the new promise api') - log.debug('starting multipart parsing') - - const req = this.raw - - const busboyOptions = deepmergeAll({ headers: req.headers }, options || {}, opts || {}) - const stream = busboy(busboyOptions) - let completed = false - let files = 0 - - req.on('error', function (err) { - stream.destroy() - if (!completed) { - completed = true - done(err) - } - }) - - stream.on('finish', function () { - log.debug('finished receiving stream, total %d files', files) - if (!completed) { - completed = true - setImmediate(done) - } - }) - - stream.on('file', wrap) - - req.pipe(stream) - .on('error', function (error) { - req.emit('error', error) - }) - - function wrap (field, file, filename, encoding, mimetype) { - log.debug({ field, filename, encoding, mimetype }, 'parsing part') - files++ - eos(file, waitForFiles) - if (field === '__proto__' || field === 'constructor') { - file.destroy(new Error(`${field} is not allowed as field name`)) - return - } - handler(field, file, filename, encoding, mimetype) - } - - function waitForFiles (err) { - if (err) { - completed = true - done(err) - } - } - - return stream + return this.raw[kMultipart] } function handleMultipart (opts = {}) { @@ -560,7 +421,9 @@ function fastifyMultipart (fastify, options, done) { function * filesFromFields (container) { try { - for (const field of Object.values(container)) { + const fields = Array.isArray(container) ? container : Object.values(container) + for (let i = 0; i < fields.length; ++i) { + const field = fields[i] if (Array.isArray(field)) { for (const subField of filesFromFields.call(this, field)) { yield subField @@ -585,7 +448,8 @@ function fastifyMultipart (fastify, options, done) { if (!this.tmpUploads) { return } - for (const filepath of this.tmpUploads) { + for (let i = 0; i < this.tmpUploads.length; ++i) { + const filepath = this.tmpUploads[i] try { await unlink(filepath) } catch (error) { diff --git a/package.json b/package.json index de45b96d..05a28ce8 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "@fastify/error": "^3.0.0", "@fastify/swagger": "^8.3.1", "@fastify/swagger-ui": "^1.8.0", - "end-of-stream": "^1.4.4", "fastify-plugin": "^4.0.0", "secure-json-parse": "^2.4.0", "stream-wormhole": "^1.1.0" @@ -48,7 +47,7 @@ "start": "CLIMEM=8999 node -r climem ./examples/example", "test": "npm run test:unit && npm run test:typescript", "test:typescript": "tsd", - "test:unit": "tap \"test/**/*.test.js\" -t 90" + "test:unit": "tap -t 90" }, "repository": { "type": "git", diff --git a/test/legacy/append-body.test.js b/test/legacy/append-body.test.js deleted file mode 100644 index e5326432..00000000 --- a/test/legacy/append-body.test.js +++ /dev/null @@ -1,833 +0,0 @@ -'use strict' -const test = require('tap').test -const FormData = require('form-data') -const Fastify = require('fastify') -const multipart = require('./../..') -const http = require('node:http') -const path = require('node:path') -const fs = require('node:fs') -const pump = require('pump') - -const filePath = path.join(__dirname, '..', '..', 'README.md') - -test('addToBody option', { skip: process.platform === 'win32' }, t => { - t.plan(8) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart, { addToBody: true }) - - fastify.post('/', function (req, reply) { - t.equal(req.body.myField, 'hello') - t.equal(req.body.myCheck, 'true') - t.match(req.body.myFile, [{ - encoding: '7bit', - filename: 'README.md', - limit: false, - mimetype: 'text/markdown' - }]) - t.type(req.body.myFile[0].data, Buffer) - t.equal(req.body.myFile[0].data.toString('utf8').substr(0, 20), '# @fastify/multipart') - - reply.send('ok') - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - const rs = fs.createReadStream(filePath) - form.append('myField', 'hello') - form.append('myCheck', 'true') - form.append('myFile', rs) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody with limit exceeded', { skip: process.platform === 'win32' }, t => { - t.plan(5) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart, { addToBody: true, limits: { fileSize: 1 } }) - - fastify.post('/', function (req, reply) { - t.equal(req.body.myFile[0].limit, true) - t.equal(req.body.myFile[0].data, undefined) - - reply.send('ok') - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - const rs = fs.createReadStream(filePath) - form.append('myFile', rs) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody option and multiple files', { skip: process.platform === 'win32' }, t => { - t.plan(7) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - let fileCounter = 0 - const opts = { - addToBody: true, - onFile: (fieldName, stream, filename, encoding, mimetype) => { - fileCounter++ - stream.resume() - } - } - fastify.register(multipart, opts) - - fastify.post('/', function (req, reply) { - t.match(req.body.myFile, [{ - data: [], - encoding: '7bit', - filename: 'README.md', - limit: false, - mimetype: 'text/markdown' - }]) - - t.match(req.body.myFileTwo, [{ - data: [], - encoding: '7bit', - filename: 'README.md', - limit: false, - mimetype: 'text/markdown' - }]) - - t.match(req.body.myFileThree, [{ - data: [], - encoding: '7bit', - filename: 'README.md', - limit: false, - mimetype: 'text/markdown' - }]) - - t.equal(fileCounter, 3, 'We must receive 3 file events') - reply.send('ok') - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - const rs1 = fs.createReadStream(filePath) - const rs2 = fs.createReadStream(filePath) - const rs3 = fs.createReadStream(filePath) - form.append('myFile', rs1) - form.append('myFileTwo', rs2) - form.append('myFileThree', rs3) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody option and multiple files in one field', { skip: process.platform === 'win32' }, t => { - t.plan(7) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - const opts = { - addToBody: true - } - fastify.register(multipart, opts) - - fastify.post('/', function (req, reply) { - t.match(req.body.myFile, [{ - encoding: '7bit', - filename: 'README.md', - limit: false, - mimetype: 'text/markdown' - }, { - encoding: '7bit', - filename: 'LICENSE', - limit: false, - mimetype: 'application/octet-stream' - }, { - encoding: '7bit', - filename: 'form.html', - limit: false, - mimetype: 'text/html' - }]) - req.body.myFile.forEach(x => { - t.equal(Buffer.isBuffer(x.data), true) - }) - reply.send('ok') - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - const rs1 = fs.createReadStream(path.join(__dirname, '..', '..', 'README.md')) - const rs2 = fs.createReadStream(path.join(__dirname, '..', '..', 'LICENSE')) - const rs3 = fs.createReadStream(path.join(__dirname, '..', '..', 'form.html')) - form.append('myFile', rs1) - form.append('myFile', rs2) - form.append('myFile', rs3) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody option and multiple strings in one field', { skip: process.platform === 'win32' }, t => { - t.plan(4) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - const opts = { - addToBody: true - } - fastify.register(multipart, opts) - - fastify.post('/', function (req, reply) { - t.match(req.body.myField, ['1', '2', '3']) - - reply.send('ok') - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - form.append('myField', '1') - form.append('myField', '2') - form.append('myField', '3') - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody option and custom stream management', { skip: process.platform === 'win32' }, t => { - t.plan(7) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - const opts = { - addToBody: true, - onFile: (fieldName, stream, filename, encoding, mimetype) => { - t.equal(fieldName, 'myFile') - stream.resume() - } - } - fastify.register(multipart, opts) - - fastify.post('/', function (req, reply) { - t.equal(req.body.myField, 'hello') - t.equal(req.body.myCheck, 'true') - t.match(req.body.myFile, [{ - data: [], - encoding: '7bit', - filename: 'README.md', - limit: false, - mimetype: 'text/markdown' - }]) - - reply.send('ok') - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - const rs = fs.createReadStream(filePath) - form.append('myField', 'hello') - form.append('myCheck', 'true') - form.append('myFile', rs) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody option with promise', { skip: process.platform === 'win32' }, t => { - t.plan(5) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - const opts = { - addToBody: true, - onFile: async (fieldName, stream, filename, encoding, mimetype) => { - await new Promise(resolve => setTimeout(resolve, 10)) - t.equal(fieldName, 'myFile') - stream.resume() - } - } - fastify.register(multipart, opts) - - fastify.post('/', function (req, reply) { - t.match(req.body.myFile, [{ - data: [], - encoding: '7bit', - filename: 'README.md', - limit: false, - mimetype: 'text/markdown' - }]) - - reply.send('ok') - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - const rs = fs.createReadStream(filePath) - form.append('myFile', rs) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody option with promise in error', { skip: process.platform === 'win32' }, t => { - t.plan(3) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - const opts = { - addToBody: true, - onFile: (fieldName, stream, filename, encoding, mimetype) => { - return Promise.reject(new Error('my error')) - } - } - fastify.register(multipart, opts) - - fastify.post('/', function (req, reply) { - t.fail('should not execute the handler') - }) - - fastify.listen({ port: 0 }, 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, 500) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - const rs = fs.createReadStream(filePath) - form.append('myFile', rs) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody with shared schema', { skip: process.platform === 'win32' }, (t) => { - t.plan(9) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart, { - addToBody: true, - sharedSchemaId: 'mySharedSchema', - onFile: (fieldName, stream, filename, encoding, mimetype) => { - t.equal(fieldName, 'myFile') - t.equal(filename, 'README.md') - t.equal(encoding, '7bit') - t.equal(mimetype, 'text/markdown') - stream.resume() - } - }) - - fastify.after(() => { - fastify.post('/', { - schema: { - body: { - type: 'object', - required: ['myField', 'myFile'], - properties: { - myField: { type: 'string' }, - myFile: { type: 'array', items: fastify.getSchema('mySharedSchema') } - } - } - } - }, function (req, reply) { - t.equal(req.body.myField, 'hello') - t.match(req.body.myFile, [{ - data: [], - encoding: '7bit', - filename: 'README.md', - limit: false, - mimetype: 'text/markdown' - }]) - reply.send('ok') - }) - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - fastify.close() - t.end() - }) - }) - - const rs = fs.createReadStream(filePath) - form.append('myField', 'hello') - form.append('myFile', rs) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody with shared schema (async/await)', { skip: process.platform === 'win32' }, async (t) => { - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - await fastify.register(multipart, { - addToBody: true, - sharedSchemaId: 'mySharedSchema', - onFile: (fieldName, stream, filename, encoding, mimetype) => { - t.equal(fieldName, 'myFile') - t.equal(filename, 'README.md') - t.equal(encoding, '7bit') - t.equal(mimetype, 'text/markdown') - stream.resume() - } - }) - - fastify.post('/', { - schema: { - body: { - type: 'object', - required: ['myField', 'myFile'], - properties: { - myField: { type: 'string' }, - myFile: { type: 'array', items: fastify.getSchema('mySharedSchema') } - } - } - } - }, function (req, reply) { - t.equal(req.body.myField, 'hello') - t.match(req.body.myFile, [{ - data: [], - encoding: '7bit', - filename: 'README.md', - limit: false, - mimetype: 'text/markdown' - }]) - reply.send('ok') - }) - - 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' - } - - return new Promise((resolve, reject) => { - const req = http.request(opts, (res) => { - t.equal(res.statusCode, 200) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - fastify.close() - resolve() - }) - }) - - const rs = fs.createReadStream(filePath) - form.append('myField', 'hello') - form.append('myFile', rs) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody with shared schema error', { skip: process.platform === 'win32' }, (t) => { - t.plan(3) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart, { - addToBody: true, - sharedSchemaId: 'mySharedSchema' - }).then(() => { - fastify.post('/', { - schema: { - body: { - type: 'object', - required: ['myField', 'myFile'], - properties: { - myField: { type: 'string' }, - myFile: { type: 'array', items: fastify.getSchema('mySharedSchema') } - } - } - } - }, function (req, reply) { - reply.send('ok') - }) - - fastify.listen({ port: 0 }, 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, 400) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - const rs = fs.createReadStream(filePath) - // missing the myField parameter - form.append('myFile', rs) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) - }) -}) - -test('addToBody without files and shared schema', { skip: process.platform === 'win32' }, t => { - t.plan(5) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - const opts = { - addToBody: true, - sharedSchemaId: 'mySharedSchema', - onFile: (fieldName, stream, filename, encoding, mimetype) => { - t.fail('there are not stream') - } - } - fastify.register(multipart, opts) - - fastify.post('/', { - schema: { - body: { - type: 'object', - required: ['myField', 'myField2'], - properties: { - myField: { type: 'string' }, - myField2: { type: 'string' } - } - } - } - }, function (req, reply) { - t.equal(req.body.myField, 'hello') - t.equal(req.body.myField2, 'world') - - reply.send('ok') - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - form.append('myField', 'hello') - form.append('myField2', 'world') - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody option does not change behaviour on not-multipart request', { skip: process.platform === 'win32' }, t => { - t.plan(2) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart, { addToBody: true }) - fastify.get('/', async (req, rep) => { rep.send('hello') }) - fastify.post('/', function (req, reply) { }) - - fastify.listen({ port: 0 }, function () { - fastify.inject({ - method: 'GET', - url: '/', - port: fastify.server.address().port - }, (err, res) => { - t.error(err) - t.equal(res.payload, 'hello') - }) - }) -}) - -test('addToBody with __proto__ field', t => { - t.plan(3) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - const opts = { - addToBody: true, - onFile: (fieldName, stream, filename, encoding, mimetype) => { - t.fail('there are not stream') - } - } - fastify.register(multipart, opts) - - fastify.post('/', function (req, reply) { - t.fail('should not be called') - }) - - fastify.listen({ port: 0 }, 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, 500) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - form.append('myField', 'hello') - form.append('__proto__', 'world') - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('addToBody with constructor field', t => { - t.plan(3) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - const opts = { - addToBody: true, - onFile: (fieldName, stream, filename, encoding, mimetype) => { - t.fail('there are not stream') - } - } - fastify.register(multipart, opts) - - fastify.post('/', function (req, reply) { - t.fail('should not be called') - }) - - fastify.listen({ port: 0 }, 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, 500) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - form.append('myField', 'hello') - form.append('constructor', 'world') - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) diff --git a/test/legacy/big.test.js b/test/legacy/big.test.js deleted file mode 100644 index c5bff3b2..00000000 --- a/test/legacy/big.test.js +++ /dev/null @@ -1,117 +0,0 @@ -'use strict' - -const test = require('tap').test -const FormData = require('form-data') -const Fastify = require('fastify') -const multipart = require('./../..') -const http = require('node:http') -const stream = require('readable-stream') -const Readable = stream.Readable -const Writable = stream.Writable -const pump = stream.pipeline -const eos = stream.finished -const crypto = require('node:crypto') - -// skipping on Github Actions because it takes too long -test('should upload a big file in constant memory', { skip: process.env.CI }, function (t) { - t.plan(12) - - const fastify = Fastify() - const hashInput = crypto.createHash('sha256') - let sent = false - - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart, { - limits: { - fileSize: Infinity, - parts: Infinity - } - }) - - fastify.post('/', function (req, reply) { - t.ok(req.isMultipart()) - - req.multipart(handler, function (err) { - t.error(err) - }) - - function handler (field, file, filename, encoding, mimetype) { - t.equal(filename, 'random-data') - t.equal(field, 'upload') - t.equal(encoding, '7bit') - t.equal(mimetype, 'binary/octet-stream') - const hashOutput = crypto.createHash('sha256') - - pump(file, hashOutput, new Writable({ - objectMode: true, - write (chunk, enc, cb) { - if (!sent) { - eos(hashInput, () => { - this._write(chunk, enc, cb) - }) - return - } - - t.equal(hashInput.digest('hex'), chunk.toString('hex')) - cb() - } - }), function (err) { - t.error(err) - - const memory = process.memoryUsage() - t.ok(memory.rss < 400 * 1024 * 1024) // 200MB - t.ok(memory.heapTotal < 400 * 1024 * 1024) // 200MB - reply.send() - }) - } - }) - - fastify.listen({ port: 0 }, function () { - const knownLength = 1024 * 1024 * 1024 - let total = knownLength - const form = new FormData({ maxDataSize: total }) - const rs = new Readable({ - read (n) { - if (n > total) { - n = total - } - - const buf = Buffer.alloc(n).fill('x') - hashInput.update(buf) - this.push(buf) - - total -= n - - if (total === 0) { - t.pass('finished generating') - sent = true - hashInput.end() - this.push(null) - } - } - }) - form.append('upload', rs, { - filename: 'random-data', - contentType: 'binary/octet-stream', - knownLength - }) - - const opts = { - protocol: 'http:', - hostname: 'localhost', - port: fastify.server.address().port, - path: '/', - headers: form.getHeaders(), - method: 'POST' - } - - const req = http.request(opts, () => { fastify.close(noop) }) - - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -function noop () { } diff --git a/test/legacy/multipart.test.js b/test/legacy/multipart.test.js deleted file mode 100644 index 5e76c113..00000000 --- a/test/legacy/multipart.test.js +++ /dev/null @@ -1,532 +0,0 @@ -'use strict' -const os = require('node:os') -const test = require('tap').test -const FormData = require('form-data') -const Fastify = require('fastify') -const multipart = require('./../..') -const http = require('node:http') -const path = require('node:path') -const fs = require('node:fs') -const concat = require('concat-stream') -const stream = require('readable-stream') -const pump = stream.pipeline -const eos = stream.finished - -const filePath = path.join(__dirname, '..', '..', 'README.md') - -test('should parse forms', { skip: process.platform === 'win32' }, function (t) { - t.plan(14) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart, { limits: { fields: 1 } }) - - fastify.post('/', function (req, reply) { - t.ok(req.isMultipart()) - - const mp = req.multipart(handler, function (err) { - t.error(err) - reply.code(200).send() - }) - - mp.on('field', function (name, value) { - t.not(name, 'willbe', 'Busboy fields limit ignored') - t.not(value, 'dropped', 'Busboy fields limit ignored') - t.equal(name, 'hello') - t.equal(value, 'world') - }) - - function handler (field, file, filename, encoding, mimetype) { - t.equal(filename, 'README.md') - t.equal(field, 'upload') - t.equal(encoding, '7bit') - t.equal(mimetype, 'text/markdown') - file.on('fieldsLimit', () => t.ok('field limit reached')) - const original = fs.readFileSync(filePath, 'utf8') - file.pipe(concat(function (buf) { - t.equal(buf.toString(), original) - })) - } - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - const rs = fs.createReadStream(filePath) - form.append('upload', rs) - form.append('hello', 'world') - form.append('willbe', 'dropped') - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('should call finished when both files are pumped', { skip: process.platform === 'win32' }, function (t) { - t.plan(10) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart) - - fastify.post('/', function (req, reply) { - let fileCount = 0 - t.ok(req.isMultipart()) - - req.multipart(handler, function (err) { - t.error(err) - t.equal(fileCount, 2) - reply.code(200).send() - }) - - function handler (field, file, filename, encoding, mimetype) { - const saveTo = path.join(os.tmpdir(), path.basename(filename)) - eos(file, function (err) { - t.error(err) - fileCount++ - }) - - pump(file, fs.createWriteStream(saveTo), function (err) { - t.error(err) - }) - } - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - form.append('upload', fs.createReadStream(filePath)) - form.append('upload2', fs.createReadStream(filePath)) - form.append('hello', 'world') - form.append('willbe', 'dropped') - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('should call finished if one of the streams closes prematurely', { skip: process.platform === 'win32' }, function (t) { - t.plan(5) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart) - - fastify.post('/', function (req, reply) { - let fileCount = 0 - t.ok(req.isMultipart()) - - req.multipart(handler, function () { - t.equal(fileCount, 1) - reply.code(200).send() - }) - - function handler (field, file, filename, encoding, mimetype) { - const saveTo = path.join(os.tmpdir(), path.basename(filename)) - eos(file, function () { - fileCount++ - }) - - file.on('data', function () { - if (fileCount === 0) { - this.destroy() - } - }) - - pump(file, fs.createWriteStream(saveTo), () => {}) - } - }) - - fastify.listen({ port: 0 }, function () { - // request - const form = new FormData() - const opts = { - protocol: 'http:', - hostname: 'localhost', - port: fastify.server.address().port, - path: '/', - headers: form.getHeaders(), - method: 'POST' - } - - const stream1 = fs.createReadStream(filePath) - - const req = http.request(opts, (res) => { - t.equal(res.statusCode, 200) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - form.append('upload1', stream1, { - filename: 'random-data1' - }) - form.append('upload2', stream1, { - filename: 'random-data2' - }) - - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('should error if it is not multipart', { skip: process.platform === 'win32' }, function (t) { - t.plan(4) - - const fastify = Fastify() - - t.teardown(fastify.close.bind(fastify)) - fastify.register(multipart) - - fastify.post('/', function (req, reply) { - t.notOk(req.isMultipart()) - - req.multipart(handler, function (err) { - t.ok(err) - t.equal(err.message, 'the request is not multipart') - reply.code(500).send() - }) - - function handler (field, file, filename, encoding, mimetype) { - t.fail('this should never be called') - } - }) - - fastify.listen({ port: 0 }, function () { - // request - const opts = { - protocol: 'http:', - hostname: 'localhost', - port: fastify.server.address().port, - headers: { - 'content-type': 'application/json' - }, - path: '/', - method: 'POST' - } - - const req = http.request(opts, (res) => { - t.equal(res.statusCode, 500) - }) - req.end(JSON.stringify({ hello: 'world' })) - }) -}) - -test('should error if handler is not a function', { skip: process.platform === 'win32' }, function (t) { - t.plan(3) - - const fastify = Fastify() - - t.teardown(fastify.close.bind(fastify)) - fastify.register(multipart) - - fastify.post('/', function (req, reply) { - const handler = null - - req.multipart(handler, function (err) { - t.ok(err) - reply.code(500).send() - }) - }) - - fastify.listen({ port: 0 }, 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) => { - res.resume() - res.on('end', () => { - t.equal(res.statusCode, 500) - t.pass('res ended successfully') - }) - }) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('should error if callback is not a function', { skip: process.platform === 'win32' }, function (t) { - t.plan(3) - - const fastify = Fastify() - - t.teardown(fastify.close.bind(fastify)) - fastify.register(multipart) - - fastify.post('/', function (req) { - const callback = null - req.multipart(handler, callback) - - function handler () {} - }) - - fastify.listen({ port: 0 }, 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) => { - res.resume() - res.on('end', () => { - t.equal(res.statusCode, 500) - t.pass('res ended successfully') - }) - }) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('should error if it is invalid multipart', { skip: process.platform === 'win32' }, function (t) { - t.plan(5) - - const fastify = Fastify() - - t.teardown(fastify.close.bind(fastify)) - fastify.register(multipart) - - fastify.post('/', function (req, reply) { - t.ok(req.isMultipart()) - - req.multipart(handler, function (err) { - t.ok(err) - t.equal(err.message, 'Multipart: Boundary not found') - reply.code(500).send() - }) - - function handler (field, file, filename, encoding, mimetype) { - t.fail('this should never be called') - } - }) - - fastify.listen({ port: 0 }, function () { - // request - const form = new FormData() - const opts = { - protocol: 'http:', - hostname: 'localhost', - port: fastify.server.address().port, - headers: { - 'content-type': 'multipart/form-data' - }, - path: '/', - method: 'POST' - } - - const req = http.request(opts, (res) => { - t.equal(res.statusCode, 500) - }) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('should override options', { skip: process.platform === 'win32' }, function (t) { - t.plan(5) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart, { limits: { fileSize: 1 } }) - - fastify.post('/', function (req, reply) { - const mp = req.multipart(handler, function (err) { - t.error(err) - reply.code(200).send() - }, { limits: { fileSize: 2 } }) - - t.equal(mp.opts.limits.fileSize, 2, 'options.limits.fileSize was updated successfully') - - function handler (field, file, filename, encoding, mimetype) { - file.pipe(concat(function (buf) { })) - } - }) - - fastify.listen({ port: 0 }, 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) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - - const rs = fs.createReadStream(filePath) - form.append('upload', rs) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('should not allow __proto__', { skip: process.platform === 'win32' }, function (t) { - t.plan(5) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart, { limits: { fields: 1 } }) - - fastify.post('/', function (req, reply) { - t.ok(req.isMultipart()) - - const mp = req.multipart(handler, function (err) { - t.equal(err.message, '__proto__ is not allowed as field name') - reply.code(500).send() - }) - - mp.on('field', function (name, value) { - t.fail('should not be called') - }) - - function handler (field, file, filename, encoding, mimetype) { - t.fail('should not be called') - } - }) - - fastify.listen({ port: 0 }, 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, 500) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - const rs = fs.createReadStream(filePath) - form.append('__proto__', rs) - // form.append('hello', 'world') - // form.append('willbe', 'dropped') - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) - -test('should not allow constructor', { skip: process.platform === 'win32' }, function (t) { - t.plan(5) - - const fastify = Fastify() - t.teardown(fastify.close.bind(fastify)) - - fastify.register(multipart, { limits: { fields: 1 } }) - - fastify.post('/', function (req, reply) { - t.ok(req.isMultipart()) - - const mp = req.multipart(handler, function (err) { - t.equal(err.message, 'constructor is not allowed as field name') - reply.code(500).send() - }) - - mp.on('field', function (name, value) { - t.fail('should not be called') - }) - - function handler (field, file, filename, encoding, mimetype) { - t.fail('should not be called') - } - }) - - fastify.listen({ port: 0 }, 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, 500) - res.resume() - res.on('end', () => { - t.pass('res ended successfully') - }) - }) - const rs = fs.createReadStream(filePath) - form.append('constructor', rs) - pump(form, req, function (err) { - t.error(err, 'client pump: no err') - }) - }) -}) diff --git a/test/multipart-attach-body.test.js b/test/multipart-attach-body.test.js index e4d45ac8..b809705b 100644 --- a/test/multipart-attach-body.test.js +++ b/test/multipart-attach-body.test.js @@ -121,6 +121,8 @@ test('should be able to attach all parsed field values and files and make it acc fastify.post('/', async function (req, reply) { t.ok(req.isMultipart()) + req.body.upload = req.body.upload.toString('utf8') + t.same(Object.keys(req.body), ['upload', 'hello']) t.equal(req.body.upload, original) @@ -307,6 +309,9 @@ test('should manage array fields', async function (t) { fastify.post('/', async function (req, reply) { t.ok(req.isMultipart()) + req.body.upload[0] = req.body.upload[0].toString('utf8') + req.body.upload[1] = req.body.upload[1].toString('utf8') + t.same(req.body, { upload: [original, original], hello: ['hello', 'world'] @@ -434,3 +439,50 @@ test('should handle file stream consumption when internal buffer is not yet load await once(res, 'end') t.pass('res ended successfully') }) + +test('should pass the buffer instead of converting to string', async function (t) { + t.plan(7) + + const fastify = Fastify() + t.teardown(fastify.close.bind(fastify)) + + fastify.register(multipart, { attachFieldsToBody: 'keyValues' }) + + const original = fs.readFileSync(filePath) + + fastify.post('/', async function (req, reply) { + t.ok(req.isMultipart()) + + t.same(Object.keys(req.body), ['upload', 'hello']) + + t.ok(req.body.upload instanceof Buffer) + t.ok(Buffer.compare(req.body.upload, original) === 0) + 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') +}) diff --git a/types/index.d.ts b/types/index.d.ts index 2e4a4a74..74853810 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,4 +1,4 @@ -import { Busboy, BusboyConfig, BusboyFileStream } from '@fastify/busboy' +import { BusboyConfig, BusboyFileStream } from '@fastify/busboy' import { FastifyPluginCallback, FastifyRequest } from 'fastify' import { Readable } from 'stream' import { FastifyErrorConstructor } from '@fastify/error' @@ -12,13 +12,6 @@ declare module 'fastify' { options?: Omit ) => AsyncIterableIterator; - // legacy - multipart: ( - handler: MultipartHandler, - next: (err: Error) => void, - options?: Omit - ) => Busboy; - // Stream mode file: ( options?: Omit @@ -110,11 +103,6 @@ declare namespace fastifyMultipart { } export interface FastifyMultipartBaseOptions { - /** - * Append the multipart parameters to the body object - */ - addToBody?: boolean; - /** * Add a shared schema to validate the input fields */ diff --git a/types/index.test-d.ts b/types/index.test-d.ts index de9712fc..d7bbea5e 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -12,25 +12,6 @@ const pump = util.promisify(pipeline) const runServer = async () => { const app = fastify() - app.register(fastifyMultipart, { - addToBody: true, - sharedSchemaId: 'sharedId', - throwFileSizeLimit: false, - // stream should be of type streams.Readable - // body should be of type fastifyMultipart.Record - onFile: (fieldName: string, stream: any, filename: string, encoding: string, mimetype: string, body: Record) => { - console.log(fieldName, stream, filename, encoding, mimetype, body) - }, - limits: { - fieldNameSize: 200, - fieldSize: 200, - fields: 200, - fileSize: 200, - files: 2, - headerPairs: 200 - } - }) - app.register(fastifyMultipart, { attachFieldsToBody: true, onFile: (part: MultipartFile) => { @@ -38,19 +19,6 @@ const runServer = async () => { } }) - app.get('/path', (request) => { - const isMultiPart = request.isMultipart() - request.multipart((field, file, filename, encoding, mimetype) => { - console.log(field, file, filename, encoding, mimetype, isMultiPart) - }, (err) => { - throw err - }, { - limits: { - fileSize: 10000 - } - }) - }) - // usage app.post('/', async (req, reply) => { const data = await req.file()