├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .gitignore ├── .travis.yml ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── multipart.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [{*.js,*.json,*.yml}] 10 | indent_size = 2 11 | indent_style = space 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | eol-last: error 3 | indent: ["error", 2, { "SwitchCase": 1 }] 4 | no-trailing-spaces: error 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "4.9" 5 | - "6.17" 6 | - "8.16" 7 | - "10.16" 8 | - "12.11" 9 | sudo: false 10 | cache: 11 | directories: 12 | - node_modules 13 | before_install: 14 | # Configure npm 15 | - | 16 | # Skip updating shrinkwrap / lock 17 | npm config set shrinkwrap false 18 | # Setup Node.js version-specific dependencies 19 | - | 20 | # eslint for linting 21 | # - remove for Node.js < 8 22 | if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 8 ]]; then 23 | npm rm --save-dev eslint 24 | fi 25 | - | 26 | # mocha for testing 27 | # - use 3.x for Node.js < 4 28 | # - use 5.x for Node.js < 6 29 | if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 4 ]]; then 30 | npm install --save-dev mocha@3.5.3 31 | elif [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 6 ]]; then 32 | npm install --save-dev mocha@5.2.0 33 | fi 34 | - | 35 | # nyc for coverage 36 | # - use 10.x for Node.js < 4 37 | # - use 11.x for Node.js < 6 38 | if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 4 ]]; then 39 | npm install --save-dev nyc@10.3.2 40 | elif [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 6 ]]; then 41 | npm install --save-dev nyc@11.9.0 42 | fi 43 | - | 44 | # supertest for http calls 45 | # - use 2.0.0 for Node.js < 4 46 | # - use 3.4.2 for Node.js < 6 47 | if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 4 ]]; then 48 | npm install --save-dev supertest@2.0.0 49 | elif [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 6 ]]; then 50 | npm install --save-dev supertest@3.4.2 51 | fi 52 | # Update Node.js modules 53 | - | 54 | # Prune & rebuild node_modules 55 | if [[ -d node_modules ]]; then 56 | npm prune 57 | npm rebuild 58 | fi 59 | 60 | script: 61 | - | 62 | # Run test script 63 | npm run-script test-ci 64 | - | 65 | # Run linting 66 | if npm -ps ls eslint | grep -q eslint; then 67 | npm run-script lint 68 | fi 69 | after_script: 70 | - | 71 | # Upload coverage to coveralls, if exists 72 | if [[ -d .nyc_output ]]; then 73 | npm install --save-dev coveralls@2 74 | nyc report --reporter=text-lcov | coveralls 75 | fi 76 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | unreleased 2 | ========== 3 | 4 | * deps: http-errors@~1.7.3 5 | - Fix error creating objects in some environments 6 | - deps: inherits@2.0.4 7 | - deps: setprototypeof@1.1.1 8 | * deps: qs@~6.7.0 9 | - Fix parsing array brackets after index 10 | * deps: safe-buffer@5.2.0 11 | * deps: type-is@~1.6.18 12 | - deps: mime-types@~2.1.24 13 | - perf: prevent internal `throw` on invalid type 14 | 15 | 2.2.0 / 2018-09-14 16 | ================== 17 | 18 | * Fix masking `multiparty` errors as 400 19 | * deps: multiparty@~4.2.1 20 | - Use http-errors for raised errors 21 | - Use uid-safe module to for temp file names 22 | - Update to fd-slicer 1.1.0 23 | - perf: remove parameter reassignment 24 | 25 | 2.1.1 / 2018-06-22 26 | ================== 27 | 28 | * deps: multiparty@~4.1.4 29 | - Enable strict mode 30 | - Fix file extension filtering stopping on certain whitespace characters 31 | - Use safe-buffer for improved API safety 32 | * deps: qs@~6.5.2 33 | * deps: type-is@~1.6.16 34 | - deps: mime-types@~2.1.18 35 | 36 | 2.1.0 / 2017-10-19 37 | ================== 38 | 39 | * deps: multiparty@~4.1.3 40 | - Use `os.tmpdir()` instead of `os.tmpDir()` 41 | - deps: fd-slicer@~1.0.1 42 | * deps: qs@~6.5.1 43 | - Fix array parsing from skipping empty values 44 | - Fix compacting of nested sparse arrays 45 | - Fix parsing & compacting very deep objects 46 | * deps: type-is@~1.6.15 47 | - Fix type error when given invalid type to match against 48 | - deps: mime-types@~2.1.15 49 | * perf: enable strict mode 50 | 51 | 2.0.0 / 2015-07-13 52 | ================== 53 | 54 | * Requires Node.js >= 0.10.0 55 | * deps: multiparty@~4.1.2 56 | * deps: on-finished@~2.3.0 57 | * deps: qs@~4.0.0 58 | * deps: type-is@~1.6.4 59 | 60 | 1.2.5 / 2014-10-14 61 | ================== 62 | 63 | * Update qs to 2.2.4 64 | * Update type-is to 1.5.2 65 | 66 | 1.2.4 / 2014-08-29 67 | ================== 68 | 69 | * Update qs to 2.2.2 70 | 71 | 1.2.3 / 2014-08-28 72 | ================== 73 | 74 | * Update qs to 2.2.1 75 | 76 | 1.2.2 / 2014-08-27 77 | ================== 78 | 79 | * Update qs to 2.2.0 80 | 81 | 1.2.1 / 2014-08-07 82 | ================== 83 | 84 | * Update multiparty to 3.3.2 85 | * Update qs to 1.2.0 86 | 87 | 1.2.0 / 2014-08-06 88 | ================== 89 | 90 | * Update multiparty to 3.3.1 91 | * Update qs to 1.1.0 92 | 93 | 1.1.0 / 2014-07-03 94 | ================== 95 | 96 | * Update multiparty to 3.3.0 97 | * Use type-is to check Content-Type 98 | 99 | 1.0.6 / 2014-07-03 100 | ================== 101 | 102 | * Fix callback hang in node.js 0.8 on errors 103 | 104 | 1.0.5 / 2014-06-01 105 | ================== 106 | 107 | * Update multiparty to 3.2.8 108 | 109 | 1.0.4 / 2014-05-26 110 | ================== 111 | 112 | * Fix error causing response to hang 113 | * Update multiparty to 3.2.6 114 | 115 | 1.0.3 / 2014-01-20 116 | ================== 117 | 118 | * Update multiparty to 3.2 119 | 120 | 1.0.2 / 2014-01-17 121 | ================== 122 | 123 | * Update multiparty to 3.1 124 | 125 | 1.0.1 / 2013-10-25 126 | ================== 127 | 128 | * Update multiparty to 3.0 129 | 130 | 1.0.0 / 2013-10-25 131 | ================== 132 | 133 | * revive 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2010 Sencha Inc. 4 | Copyright (c) 2011 TJ Holowaychuk 5 | Copyright (c) 2013 Andrew Kelley 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > **This repository is archived and no longer actively maintained.** 3 | > 4 | > We are no longer accepting issues, feature requests, or pull requests. 5 | > For additional support or questions, please visit the [Express.js Discussions page](https://github.com/expressjs/express/discussions). 6 | 7 | # connect-multiparty 8 | 9 | [![NPM Version][npm-image]][npm-url] 10 | [![NPM Downloads][downloads-image]][downloads-url] 11 | [![Build Status][travis-image]][travis-url] 12 | [![Test Coverage][coveralls-image]][coveralls-url] 13 | 14 | [connect](https://github.com/senchalabs/connect/) middleware for 15 | [multiparty](https://github.com/andrewrk/node-multiparty/). 16 | 17 | I actually recommend against using this module. It's cleaner to use the 18 | multiparty API directly. 19 | 20 | This middleware will create temp files on your server and never clean them 21 | up. Thus you should not add this middleware to all routes; only to the ones 22 | in which you want to accept uploads. And in these endpoints, be sure to 23 | delete all temp files, even the ones that you don't use. 24 | 25 | ## Usage 26 | 27 | ```js 28 | var multipart = require('connect-multiparty'); 29 | var multipartMiddleware = multipart(); 30 | app.post('/upload', multipartMiddleware, function(req, resp) { 31 | console.log(req.body, req.files); 32 | // don't forget to delete all req.files when done 33 | }); 34 | ``` 35 | 36 | If you pass options to `multipart()`, they are passed directly into 37 | multiparty. 38 | 39 | ## License 40 | 41 | [MIT](LICENSE) 42 | 43 | [coveralls-image]: https://img.shields.io/coveralls/expressjs/connect-multiparty/master.svg 44 | [coveralls-url]: https://coveralls.io/r/expressjs/connect-multiparty?branch=master 45 | [downloads-image]: https://img.shields.io/npm/dm/connect-multiparty.svg 46 | [downloads-url]: https://npmjs.org/package/connect-multiparty 47 | [npm-image]: https://img.shields.io/npm/v/connect-multiparty.svg 48 | [npm-url]: https://npmjs.org/package/connect-multiparty 49 | [travis-image]: https://img.shields.io/travis/expressjs/connect-multiparty/master.svg 50 | [travis-url]: https://travis-ci.org/expressjs/connect-multiparty 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * connect-multiparty 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * Copyright(c) 2013 Andrew Kelley 6 | * MIT Licensed 7 | */ 8 | 9 | 'use strict' 10 | 11 | /** 12 | * Module dependencies. 13 | * @private 14 | */ 15 | 16 | var createError = require('http-errors') 17 | var multiparty = require('multiparty'); 18 | var onFinished = require('on-finished'); 19 | var qs = require('qs'); 20 | var typeis = require('type-is'); 21 | 22 | /** 23 | * Module exports. 24 | * @public 25 | */ 26 | 27 | module.exports = multipart 28 | 29 | /** 30 | * Parse multipart/form-data request bodies, providing the parsed 31 | * object as `req.body` and `req.files`. 32 | * 33 | * The options passed are merged with [multiparty](https://github.com/pillarjs/multiparty)'s 34 | * `Form` object, allowing you to configure the upload directory, 35 | * size limits, etc. For example if you wish to change the upload 36 | * dir do the following: 37 | * 38 | * app.use(multipart({ uploadDir: path })) 39 | * 40 | * @param {Object} options 41 | * @return {Function} 42 | * @public 43 | */ 44 | 45 | function multipart (options) { 46 | options = options || {}; 47 | 48 | return function multipart(req, res, next) { 49 | if (req._body) return next(); 50 | req.body = req.body || {}; 51 | req.files = req.files || {}; 52 | 53 | // ignore GET 54 | if ('GET' === req.method || 'HEAD' === req.method) return next(); 55 | 56 | // check Content-Type 57 | if (!typeis(req, 'multipart/form-data')) return next(); 58 | 59 | // flag as parsed 60 | req._body = true; 61 | 62 | // parse 63 | var form = new multiparty.Form(options); 64 | var data = {}; 65 | var files = {}; 66 | var done = false; 67 | 68 | function ondata(name, val, data){ 69 | if (Array.isArray(data[name])) { 70 | data[name].push(val); 71 | } else if (data[name]) { 72 | data[name] = [data[name], val]; 73 | } else { 74 | data[name] = val; 75 | } 76 | } 77 | 78 | form.on('field', function(name, val){ 79 | ondata(name, val, data); 80 | }); 81 | 82 | form.on('file', function(name, val){ 83 | val.name = val.originalFilename; 84 | val.type = val.headers['content-type'] || null; 85 | ondata(name, val, files); 86 | }); 87 | 88 | form.on('error', function(err){ 89 | if (done) return; 90 | 91 | done = true; 92 | 93 | // set status code on error 94 | var error = createError(400, err) 95 | 96 | if (!req.readable) return next(error) 97 | 98 | // read off entire request 99 | req.resume(); 100 | onFinished(req, function(){ 101 | next(error) 102 | }); 103 | }); 104 | 105 | form.on('close', function() { 106 | if (done) return; 107 | 108 | done = true; 109 | 110 | // expand names with qs & assign 111 | req.body = qs.parse(data, { allowDots: true }) 112 | req.files = qs.parse(files, { allowDots: true }) 113 | 114 | next() 115 | }); 116 | 117 | form.parse(req); 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-multiparty", 3 | "version": "2.2.0", 4 | "description": "multipart parsing middleware for connect using multiparty", 5 | "author": "Andrew Kelley ", 6 | "contributors": [ 7 | "Douglas Christopher Wilson " 8 | ], 9 | "license": "MIT", 10 | "repository": "expressjs/connect-multiparty", 11 | "dependencies": { 12 | "http-errors": "~1.7.3", 13 | "multiparty": "~4.2.1", 14 | "on-finished": "~2.3.0", 15 | "qs": "~6.7.0", 16 | "type-is": "~1.6.18" 17 | }, 18 | "engines": { 19 | "node": ">=0.10.0" 20 | }, 21 | "devDependencies": { 22 | "connect": "3.7.0", 23 | "deep-equal": "1.0.1", 24 | "eslint": "6.5.1", 25 | "mocha": "6.2.1", 26 | "nyc": "14.1.1", 27 | "safe-buffer": "5.2.0", 28 | "supertest": "4.0.2" 29 | }, 30 | "files": [ 31 | "HISTORY.md", 32 | "LICENSE", 33 | "README.md", 34 | "index.js" 35 | ], 36 | "scripts": { 37 | "lint": "eslint .", 38 | "test": "mocha --reporter spec", 39 | "test-ci": "nyc --reporter=text npm test", 40 | "test-cov": "nyc --reporter=html --reporter=text npm test" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/multipart.js: -------------------------------------------------------------------------------- 1 | 2 | process.env.NODE_ENV = 'test'; 3 | 4 | var Buffer = require('safe-buffer').Buffer 5 | var connect = require('connect'); 6 | var deepEqual = require('deep-equal') 7 | var multipart = require('..'); 8 | var request = require('supertest'); 9 | 10 | describe('multipart()', function(){ 11 | it('should ignore GET', function(done){ 12 | request(createServer()) 13 | .get('/body') 14 | .field('user', 'Tobi') 15 | .expect(200, {}, done) 16 | }) 17 | 18 | describe('with multipart/form-data', function(){ 19 | it('should populate req.body', function(done){ 20 | request(createServer()) 21 | .post('/body') 22 | .field('user', 'Tobi') 23 | .expect(200, { user: 'Tobi' }, done) 24 | }) 25 | 26 | it('should handle duplicated middleware', function (done) { 27 | var app = connect() 28 | .use(multipart()) 29 | .use(multipart()) 30 | .use(function (req, res) { 31 | res.setHeader('Content-Type', 'application/json; charset=utf-8') 32 | res.end(JSON.stringify(req.body)) 33 | }) 34 | 35 | request(app) 36 | .post('/body') 37 | .field('user', 'Tobi') 38 | .expect(200, { user: 'Tobi' }, done) 39 | }) 40 | 41 | it('should support files', function(done){ 42 | request(createServer()) 43 | .post('/files') 44 | .attach('text', Buffer.from('some text here'), 'foo.txt') 45 | .expect(200) 46 | .expect(shouldDeepIncludeInBody({ 47 | text: { 48 | name: 'foo.txt', 49 | originalFilename: 'foo.txt', 50 | size: 14, 51 | type: 'text/plain' 52 | } 53 | })) 54 | .end(done) 55 | }) 56 | 57 | it('should work with multiple fields', function(done){ 58 | request(createServer()) 59 | .post('/body') 60 | .field('user', 'Tobi') 61 | .field('age', '1') 62 | .expect(200, { user: 'Tobi', age: '1' }, done) 63 | }) 64 | 65 | it('should handle duplicated fields', function (done) { 66 | request(createServer()) 67 | .post('/body') 68 | .field('user', 'Tobi') 69 | .field('user', 'Loki') 70 | .field('user', 'Poki') 71 | .expect(200, { user: [ 'Tobi', 'Loki', 'Poki' ] }, done) 72 | }) 73 | 74 | it('should support nesting', function(done){ 75 | request(createServer()) 76 | .post('/body') 77 | .field('user[name][first]', 'tobi') 78 | .field('user[name][last]', 'holowaychuk') 79 | .field('user[age]', '1') 80 | .field('species', 'ferret') 81 | .expect(200, { 82 | species: 'ferret', 83 | user: { 84 | age: '1', 85 | name: { first: 'tobi', last: 'holowaychuk' } 86 | } 87 | }, done) 88 | }) 89 | 90 | it('should support multiple files of the same name', function(done){ 91 | request(createServer()) 92 | .post('/files') 93 | .attach('text', Buffer.from('some text here'), 'foo.txt') 94 | .attach('text', Buffer.from('some more text stuff'), 'bar.txt') 95 | .expect(200) 96 | .expect(shouldDeepIncludeInBody({ 97 | text: [ 98 | { name: 'foo.txt' }, 99 | { name: 'bar.txt' } 100 | ] 101 | })) 102 | .end(done) 103 | }) 104 | 105 | it('should support nested files', function(done){ 106 | request(createServer()) 107 | .post('/files') 108 | .attach('docs[foo]', Buffer.from('some text here'), 'foo.txt') 109 | .attach('docs[bar]', Buffer.from('some more text stuff'), 'bar.txt') 110 | .expect(200) 111 | .expect(shouldDeepIncludeInBody({ 112 | docs: { 113 | foo: { name: 'foo.txt' }, 114 | bar: { name: 'bar.txt' } 115 | } 116 | })) 117 | .end(done) 118 | }) 119 | 120 | it('should next(err) on multipart failure', function(done){ 121 | var app = createServer() 122 | 123 | var test = request(app).post('/') 124 | test.set('Content-Type', 'multipart/form-data; boundary=foo'); 125 | test.write('--foo\r\n'); 126 | test.write('Content-filename="foo.txt"\r\n'); 127 | test.write('\r\n'); 128 | test.write('some text here'); 129 | test.write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n'); 130 | test.write('\r\n'); 131 | test.write('some more text stuff'); 132 | test.write('\r\n--foo--'); 133 | test.expect(400, 'BadRequestError: Expected alphabetic character, received 61', done) 134 | }) 135 | 136 | it('should not hang request on failure', function(done){ 137 | var app = createServer() 138 | var buf = Buffer.alloc(1024 * 10, '.') 139 | 140 | var test = request(app).post('/') 141 | test.set('Content-Type', 'multipart/form-data; boundary=foo'); 142 | test.write('--foo\r\n'); 143 | test.write('Content-filename="foo.txt"\r\n'); 144 | test.write('\r\n'); 145 | test.write('some text here'); 146 | test.write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n'); 147 | test.write('\r\n'); 148 | test.write('some more text stuff'); 149 | test.write('\r\n--foo--'); 150 | test.write(buf) 151 | test.write(buf) 152 | test.write(buf) 153 | test.expect(400, 'BadRequestError: Expected alphabetic character, received 61', done) 154 | }) 155 | 156 | it('should default req.files to {}', function(done){ 157 | request(createServer()) 158 | .post('/body') 159 | .expect(200, {}, done) 160 | }) 161 | 162 | it('should return 413 on maxFilesSize exceeded', function (done) { 163 | var max = Math.pow(2, 9) 164 | 165 | request(createServer({ maxFilesSize: max })) 166 | .post('/files') 167 | .field('user[name]', 'Tobi') 168 | .attach('text', Buffer.alloc(max + 1, 'x'), 'foo.txt') 169 | .expect(413, done) 170 | }) 171 | }) 172 | }) 173 | 174 | function createServer (opts) { 175 | var app = connect() 176 | 177 | app.use(multipart(opts)) 178 | 179 | app.use('/body', function (req, res) { 180 | res.setHeader('Content-Type', 'application/json; charset=utf-8') 181 | res.end(JSON.stringify(req.body)) 182 | }) 183 | 184 | app.use('/files', function (req, res) { 185 | res.setHeader('Content-Type', 'application/json; charset=utf-8') 186 | res.end(JSON.stringify(req.files)) 187 | }) 188 | 189 | app.use(function (err, req, res, next) { 190 | res.statusCode = err.statusCode || err.status || 500 191 | res.end(err.name + ': ' + err.message) 192 | }) 193 | 194 | return app 195 | } 196 | 197 | function shouldDeepIncludeInBody (obj) { 198 | return function (res) { 199 | deepEqual(res.body, obj) 200 | } 201 | } 202 | --------------------------------------------------------------------------------