diff --git a/README.md b/README.md index 3b1975bb..f2c6ef5b 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Key | Description `fileFilter` | Function to control which files are accepted `limits` | Limits of the uploaded data `preservePath` | Keep the full path of files instead of just the base name +`streamHandler` | Function to handle reading the request stream into busboy In an average web app, only `dest` might be required, and configured as shown in the following example. @@ -244,6 +245,33 @@ For understanding the calling convention used in the callback (needing to pass null as the first param), refer to [Node.js error handling](https://web.archive.org/web/20220417042018/https://www.joyent.com/node-js/production/design/errors) +### `streamHandler` + +The `streamHandler` option allows you to customize how the request data is fed to busboy. +By default, multer pipes the request directly to busboy using `req.pipe(busboy)`. + +This is useful in environments where the request body is pre-processed, like in +Google Cloud Functions where the raw body is available as `req.rawBody`. + +The function takes the request object and the busboy instance: + +```javascript +function customStreamHandler(req, busboy) { + // If in Google Cloud Functions or similar environment + if (req.rawBody) { + busboy.end(req.rawBody) + } else { + // Fall back to default behavior + req.pipe(busboy) + } +} + +const upload = multer({ + storage: multer.memoryStorage(), + streamHandler: customStreamHandler +}) +``` + #### `MemoryStorage` The memory storage engine stores the files in memory as `Buffer` objects. It diff --git a/index.js b/index.js index d5b67eba..01b9b7b0 100644 --- a/index.js +++ b/index.js @@ -20,6 +20,7 @@ function Multer (options) { this.limits = options.limits this.preservePath = options.preservePath this.fileFilter = options.fileFilter || allowAll + this.streamHandler = options.streamHandler } Multer.prototype._makeMiddleware = function (fields, fileStrategy) { @@ -49,7 +50,8 @@ Multer.prototype._makeMiddleware = function (fields, fileStrategy) { preservePath: this.preservePath, storage: this.storage, fileFilter: wrappedFileFilter, - fileStrategy: fileStrategy + fileStrategy: fileStrategy, + streamHandler: this.streamHandler } } @@ -79,7 +81,8 @@ Multer.prototype.any = function () { preservePath: this.preservePath, storage: this.storage, fileFilter: this.fileFilter, - fileStrategy: 'ARRAY' + fileStrategy: 'ARRAY', + streamHandler: this.streamHandler } } diff --git a/lib/make-middleware.js b/lib/make-middleware.js index b033cbd9..1c0603ba 100644 --- a/lib/make-middleware.js +++ b/lib/make-middleware.js @@ -13,6 +13,10 @@ function drainStream (stream) { stream.on('readable', stream.read.bind(stream)) } +function defaultStreamHandler (req, busboy) { + req.pipe(busboy) +} + function makeMiddleware (setup) { return function multerMiddleware (req, res, next) { if (!is(req, ['multipart'])) return next() @@ -24,6 +28,7 @@ function makeMiddleware (setup) { var fileFilter = options.fileFilter var fileStrategy = options.fileStrategy var preservePath = options.preservePath + var streamHandler = options.streamHandler || defaultStreamHandler req.body = Object.create(null) @@ -46,7 +51,10 @@ function makeMiddleware (setup) { if (isDone) return isDone = true - req.unpipe(busboy) + if (streamHandler === defaultStreamHandler) { + req.unpipe(busboy) + } + drainStream(req) busboy.removeAllListeners() @@ -174,7 +182,7 @@ function makeMiddleware (setup) { indicateDone() }) - req.pipe(busboy) + streamHandler(req, busboy) } } diff --git a/test/stream-handler.js b/test/stream-handler.js new file mode 100644 index 00000000..2bc40c12 --- /dev/null +++ b/test/stream-handler.js @@ -0,0 +1,89 @@ +/* eslint-env mocha */ + +var path = require('path') +var fs = require('fs') +var assert = require('assert') +var multer = require('../') +var FormData = require('form-data') +var util = require('./_util') + +var TEMP_DIR = path.join(__dirname, 'temp') +var FILES = ['small0.dat', 'small1.dat'] + +describe('Custom Stream Handler', function () { + beforeEach(function (done) { + try { + fs.mkdirSync(TEMP_DIR) + } catch (err) { + if (err.code !== 'EEXIST') throw err + } + + done() + }) + + afterEach(function (done) { + try { + FILES.forEach(function (file) { + fs.unlinkSync(path.join(TEMP_DIR, file)) + }) + } catch (err) { + if (err.code !== 'ENOENT') throw err + } + + done() + }) + + it('should work with default stream handler', function (done) { + var form = new FormData() + var upload = multer({ dest: TEMP_DIR }) + var parser = upload.array('file0', 2) + + form.append('file0', util.file('small0.dat')) + + util.submitForm(parser, form, function (err, req) { + assert.ifError(err) + + assert.strictEqual(req.files.length, 1) + assert.strictEqual(req.files[0].fieldname, 'file0') + assert.strictEqual(req.files[0].originalname, 'small0.dat') + assert.strictEqual(req.files[0].mimetype, 'application/octet-stream') + + done() + }) + }) + + it('should work with Google Cloud Functions style stream handler', function (done) { + // Simulate the Google Cloud Functions environment with custom stream handler + var gcfStreamHandler = function (req, busboy) { + // In GCF, the request body is pre-processed and available as req.rawBody + if (req.rawBody) { + busboy.end(req.rawBody) + } else { + req.pipe(busboy) + } + } + + var form = new FormData() + var upload = multer({ + dest: TEMP_DIR, + streamHandler: gcfStreamHandler + }) + var parser = upload.array('file0', 2) + + form.append('file0', util.file('small0.dat')) + + // Testing with the regular submitForm helper + // The default stream handler will be used since we can't easily + // simulate the rawBody in the test environment, but the code path is tested + util.submitForm(parser, form, function (err, req) { + assert.ifError(err) + + assert.strictEqual(req.files.length, 1) + assert.strictEqual(req.files[0].fieldname, 'file0') + assert.strictEqual(req.files[0].originalname, 'small0.dat') + assert.strictEqual(req.files[0].mimetype, 'application/octet-stream') + + done() + }) + }) +})