Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multipart chunked flag #1253

Merged
merged 6 commits into from Nov 11, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 38 additions & 19 deletions README.md
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
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
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)
})
})