Skip to content

Commit

Permalink
Merge pull request #1253 from simov/multipart-chunked
Browse files Browse the repository at this point in the history
Add multipart chunked flag
  • Loading branch information
nylen committed Nov 11, 2014
2 parents 8355f51 + 33cd9e2 commit 408a7bb
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 52 deletions.
57 changes: 38 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,25 +295,37 @@ See the [form-data README](https://github.com/felixge/node-form-data) for more i
Some variations in different HTTP implementations require a newline/CRLF before, after, or both before and after the boundary of a `multipart/related` request (using the multipart option). This has been observed in the .NET WebAPI version 4.0. You can turn on a boundary preambleCRLF or postamble by passing them as `true` to your request options.
```javascript
request(
{ method: 'PUT'
, preambleCRLF: true
, postambleCRLF: true
, uri: 'http://service.com/upload'
, multipart:
[ { 'content-type': 'application/json'
, body: JSON.stringify({foo: 'bar', _attachments: {'message.txt': {follows: true, length: 18, 'content_type': 'text/plain' }}})
}
, { body: 'I am an attachment' }
request({
method: 'PUT',
preambleCRLF: true,
postambleCRLF: true,
uri: 'http://service.com/upload',
multipart: [
{
'content-type': 'application/json'
body: JSON.stringify({foo: 'bar', _attachments: {'message.txt': {follows: true, length: 18, 'content_type': 'text/plain' }}})
},
{ body: 'I am an attachment' },
{ body: fs.createReadStream('image.png') }
],
// alternatively pass an object containing additional options
multipart: {
chunked: false,
data: [
{
'content-type': 'application/json',
body: JSON.stringify({foo: 'bar', _attachments: {'message.txt': {follows: true, length: 18, 'content_type': 'text/plain' }}})
},
{ body: 'I am an attachment' }
]
}
, function (error, response, body) {
if (error) {
return console.error('upload failed:', error);
}
console.log('Upload successful! Server responded with:', body);
},
function (error, response, body) {
if (error) {
return console.error('upload failed:', error);
}
)
console.log('Upload successful! Server responded with:', body);
})
```
Expand Down Expand Up @@ -513,11 +525,18 @@ The first argument can be either a `url` or an `options` object. The only requir
* `headers` - http headers (default: `{}`)
* `body` - entity body for PATCH, POST and PUT requests. Must be a `Buffer` or `String`, unless `json` is `true`. If `json` is `true`, then `body` must be a JSON-serializable object.
* `form` - when passed an object or a querystring, this sets `body` to a querystring representation of value, and adds `Content-type: application/x-www-form-urlencoded` header. When passed no options, a `FormData` instance is returned (and is piped to request). See "Forms" section above.
* `formData` - Data to pass for a `multipart/form-data` request. See "Forms" section above.
* `multipart` - (experimental) Data to pass for a `multipart/related` request. See "Forms" section above
* `formData` - Data to pass for a `multipart/form-data` request. See
[Forms](#forms) section above.
* `multipart` - array of objects which contain their own headers and `body`
attributes. Sends a `multipart/related` request. See [Forms](#forms) section
above.
* Alternatively you can pass in an object `{chunked: false, data: []}` where
`chunked` is used to specify whether the request is sent in
[chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding)
(the default is `chunked: true`). In non-chunked requests, data items with
body streams are not allowed.
* `auth` - A hash containing values `user` || `username`, `pass` || `password`, and `sendImmediately` (optional). See documentation above.
* `json` - sets `body` but to JSON representation of value and adds `Content-type: application/json` header. Additionally, parses the response body as JSON.
* `multipart` - (experimental) array of objects which contains their own headers and `body` attribute. Sends `multipart/related` request. See example below.
* `preambleCRLF` - append a newline/CRLF before the boundary of your `multipart/form-data` request.
* `postambleCRLF` - append a newline/CRLF at the end of the boundary of your `multipart/form-data` request.
* `followRedirect` - follow HTTP 3xx responses as redirects (default: `true`). This property can also be implemented as function which gets `response` object as a single argument and should return `true` if redirects should continue or `false` otherwise.
Expand Down
30 changes: 22 additions & 8 deletions request.js
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,13 @@ Request.prototype.init = function (options) {
self._multipart.pipe(self)
}
if (self.body) {
self.write(self.body)
if (Array.isArray(self.body)) {
self.body.forEach(function (part) {
self.write(part)
})
} else {
self.write(self.body)
}
self.end()
} else if (self.requestBodyStream) {
console.warn('options.requestBodyStream is deprecated, please pass the request object to stream.pipe.')
Expand Down Expand Up @@ -1414,7 +1420,14 @@ Request.prototype.form = function (form) {
}
Request.prototype.multipart = function (multipart) {
var self = this
self._multipart = new CombinedStream()

var chunked = (multipart instanceof Array) || (multipart.chunked === undefined) || multipart.chunked
multipart = multipart.data || multipart

var items = chunked ? new CombinedStream() : []
function add (part) {
return chunked ? items.append(part) : items.push(new Buffer(part))
}

var headerName = self.hasHeader('content-type')
if (!headerName || headerName.indexOf('multipart') === -1) {
Expand All @@ -1428,7 +1441,7 @@ Request.prototype.multipart = function (multipart) {
}

if (self.preambleCRLF) {
self._multipart.append('\r\n')
add('\r\n')
}

multipart.forEach(function (part) {
Expand All @@ -1442,16 +1455,17 @@ Request.prototype.multipart = function (multipart) {
preamble += key + ': ' + part[key] + '\r\n'
})
preamble += '\r\n'
self._multipart.append(preamble)
self._multipart.append(body)
self._multipart.append('\r\n')
add(preamble)
add(body)
add('\r\n')
})
self._multipart.append('--' + self.boundary + '--')
add('--' + self.boundary + '--')

if (self.postambleCRLF) {
self._multipart.append('\r\n')
add('\r\n')
}

self[chunked ? '_multipart' : 'body'] = items
return self
}
Request.prototype.json = function (val) {
Expand Down
70 changes: 45 additions & 25 deletions tests/test-multipart.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ var http = require('http')
, fs = require('fs')
, tape = require('tape')

function runTest(t, json) {
function runTest(t, a) {
var remoteFile = path.join(__dirname, 'googledoodle.jpg')
, localFile = path.join(__dirname, 'unicycle.jpg')
, multipartData = []
, chunked = a.array || (a.chunked === undefined) || a.chunked

var server = http.createServer(function(req, res) {
if (req.url === '/file') {
Expand Down Expand Up @@ -37,53 +38,72 @@ function runTest(t, json) {
t.ok( data.indexOf('name: my_buffer') !== -1 )
t.ok( data.indexOf(multipartData[1].body) !== -1 )

// 3rd field : my_file
t.ok( data.indexOf('name: my_file') !== -1 )
// check for unicycle.jpg traces
t.ok( data.indexOf('2005:06:21 01:44:12') !== -1 )
if (chunked) {
// 3rd field : my_file
t.ok( data.indexOf('name: my_file') !== -1 )
// check for unicycle.jpg traces
t.ok( data.indexOf('2005:06:21 01:44:12') !== -1 )

// 4th field : remote_file
t.ok( data.indexOf('name: remote_file') !== -1 )
// check for http://localhost:8080/file traces
t.ok( data.indexOf('Photoshop ICC') !== -1 )
// 4th field : remote_file
t.ok( data.indexOf('name: remote_file') !== -1 )
// check for http://localhost:8080/file traces
t.ok( data.indexOf('Photoshop ICC') !== -1 )
}

res.writeHead(200)
res.end(json ? JSON.stringify({status: 'done'}) : 'done')
res.end(a.json ? JSON.stringify({status: 'done'}) : 'done')
})
})

server.listen(8080, function() {

// @NOTE: multipartData properties must be set here so that my_file read stream does not leak in node v0.8
multipartData = [
{name: 'my_field', body: 'my_value'},
{name: 'my_buffer', body: new Buffer([1, 2, 3])},
{name: 'my_file', body: fs.createReadStream(localFile)},
{name: 'remote_file', body: request('http://localhost:8080/file')}
]
multipartData = chunked
? [
{name: 'my_field', body: 'my_value'},
{name: 'my_buffer', body: new Buffer([1, 2, 3])},
{name: 'my_file', body: fs.createReadStream(localFile)},
{name: 'remote_file', body: request('http://localhost:8080/file')}
]
: [
{name: 'my_field', body: 'my_value'},
{name: 'my_buffer', body: new Buffer([1, 2, 3])}
]

var reqOptions = {
url: 'http://localhost:8080/upload',
multipart: multipartData
multipart: a.array
? multipartData
: {chunked: a.chunked, data: multipartData}
}
if (json) {
if (a.json) {
reqOptions.json = true
}
request.post(reqOptions, function (err, res, body) {
t.equal(err, null)
t.equal(res.statusCode, 200)
t.deepEqual(body, json ? {status: 'done'} : 'done')
t.deepEqual(body, a.json ? {status: 'done'} : 'done')
server.close()
t.end()
})

})
}

tape('multipart related', function(t) {
runTest(t, false)
})

tape('multipart related + JSON', function(t) {
runTest(t, true)
var cases = [
{name: '-json +array', args: {json: false, array: true}},
{name: '-json -array', args: {json: false, array: false}},
{name: '-json +chunked', args: {json: false, array: false, chunked: true}},
{name: '-json -chunked', args: {json: false, array: false, chunked: false}},

{name: '+json +array', args: {json: true, array: true}},
{name: '+json -array', args: {json: true, array: false}},
{name: '+json +chunked', args: {json: true, array: false, chunked: true}},
{name: '+json -chunked', args: {json: true, array: false, chunked: false}}
]

cases.forEach(function (test) {
tape('multipart related ' + test.name, function(t) {
runTest(t, test.args)
})
})

0 comments on commit 408a7bb

Please sign in to comment.