Skip to content

Commit e0743f1

Browse files
committed
Support Buffer & Stream payloads in REST services
The "Body" payload property of REST-based services (like Amazon S3) will now return Buffer objects instead of Strings, which allows for the downloading of binary data: ```js s3.client.getObject({Bucket:'foo',Key:'bar'}, function(err, data) { console.log(data.Body); // data.Body is now a Buffer object }); ``` In addition, you can now assign a readable Stream object to the Body payload: ```js // Read the body from a stream var writeParams = {Bucket:'bucket',Key:'image.jpg',Body:stream}; var stream = fs.createReadStream('/path/to/image.jpg'); s3.client.putObject(writeParams, function() { // Write the body Buffer object to a file var readParams = {Bucket:'bucket',Key:'image.jpg'}; s3.client.getObject(readParams, function(err, data) { var out = fs.createWriteStream('/path/to/image2.jpg'); out.write(data.Body); out.end(); }) }); ``` Conflicts: lib/service_interface/query.js Fixes #3
2 parents f345ce8 + 8c0bb4b commit e0743f1

File tree

17 files changed

+93
-50
lines changed

17 files changed

+93
-50
lines changed

features/s3/objects.feature

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,17 @@ Feature: Working with Objects in S3
3333
When I delete the object with the key "hello"
3434
Then the object with the key "hello" should not exist
3535

36+
@buffer
37+
Scenario: Buffers and streams
38+
When I write buffer "world" to the key "hello"
39+
Then the object with the key "hello" should exist
40+
And the object with the key "hello" should contain "world"
41+
And I delete the object with the key "hello"
42+
43+
When I write file "testfile.txt" to the key "hello"
44+
Then the object with the key "hello" should exist
45+
And the object with the key "hello" should contain "CONTENTS OF FILE"
46+
And I delete the object with the key "hello"
47+
3648
# final step here needs to happen to cleanup the shared bucket
3749
And I delete the shared bucket

features/s3/step_definitions/objects.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,17 @@ module.exports = function () {
3131
this.s3.createBucket({Bucket:this.sharedBucket}, function(err, data) {
3232
callback();
3333
});
34-
3534
});
3635

37-
this.When(/^I write "([^"]*)" to the key "([^"]*)"$/, function(contents, key, next) {
38-
var params = {Bucket:this.sharedBucket,Key:key,Body:contents};
36+
this.When(/^I write (buffer )?"([^"]*)" to the key "([^"]*)"$/, function(buffer, contents, key, next) {
37+
var params = {Bucket: this.sharedBucket, Key: key, Body: buffer ? new Buffer(contents) : contents};
3938
this.request('s3', 'putObject', params, next);
4039
});
4140

4241
this.Then(/^the object with the key "([^"]*)" should contain "([^"]*)"$/, function(key, contents, next) {
4342
this.eventually(next, function (retry) {
4443
this.s3.getObject({Bucket:this.sharedBucket,Key:key}, function(err, data) {
45-
if (data && data.Body == contents)
44+
if (data && data.Body.toString().replace("\n", "") == contents)
4645
next();
4746
else
4847
retry();
@@ -68,9 +67,15 @@ module.exports = function () {
6867
});
6968
});
7069

70+
this.When(/^I write file "([^"]*)" to the key "([^"]*)"$/, function(filename, key, next) {
71+
var fs = require('fs');
72+
var params = {Bucket: this.sharedBucket, Key: key, Body:
73+
fs.createReadStream(__dirname + '/../../support/fixtures/' + filename)};
74+
this.request('s3', 'putObject', params, next);
75+
});
76+
7177
// this scenario is a work around for not having an after all hook
7278
this.Then(/^I delete the shared bucket$/, function(next) {
7379
this.request('s3', 'deleteBucket', {Bucket:this.sharedBucket}, next);
7480
});
75-
7681
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CONTENTS OF FILE

features/support/helpers.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,15 @@ module.exports = {
7171
* finish execution before moving onto the next step in the scenario.
7272
*/
7373
request: function request(svc, operation, params, next) {
74-
this[svc][operation](params).on('complete', function (resp) {
75-
if (resp.error) {
76-
this.unexpectedError(resp, next);
74+
var world = this;
75+
this[svc][operation](params, function(err, data) {
76+
if (err) {
77+
world.unexpectedError(this, next);
7778
} else {
78-
this.resp = resp;
79+
world.resp = this;
7980
next();
8081
}
81-
}, { bind: this }).send();
82+
});
8283
},
8384

8485
/**
@@ -87,8 +88,8 @@ module.exports = {
8788
* operation failed.
8889
*/
8990
unexpectedError: function unexpectedError(resp, next) {
90-
var svc = resp.service.serviceName;
91-
var op = resp.method;
91+
var svc = resp.request.client.serviceName;
92+
var op = resp.request.operation;
9293
var code = resp.error.code;
9394
var msg = resp.error.message;
9495
var err = 'Received unexpected error from ' + svc + '.' + op + ', ' + code + ': ' + msg;

lib/event_listeners.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,19 @@ AWS.EventListeners = {
8282
function HTTP_HEADERS(statusCode, headers, resp) {
8383
resp.httpResponse.statusCode = statusCode;
8484
resp.httpResponse.headers = headers;
85-
resp.httpResponse.body = '';
85+
resp.httpResponse.body = new Buffer('');
86+
resp.httpResponse.buffers = [];
8687
});
8788

8889
add('HTTP_DATA', 'httpData', function HTTP_DONE(chunk, resp) {
89-
resp.httpResponse.body += chunk;
90+
resp.httpResponse.buffers.push(chunk);
9091
});
9192

9293
add('HTTP_DONE', 'httpDone', function HTTP_DONE(resp) {
94+
// convert buffers array into single buffer
95+
resp.httpResponse.body = Buffer.concat(resp.httpResponse.buffers);
96+
delete resp.httpResponse.buffers;
97+
9398
this.completeRequest(resp);
9499
});
95100

lib/http.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
var AWS = require('./core');
17+
var Stream = require('stream').Stream;
1718
var inherit = AWS.util.inherit;
1819

1920
/**
@@ -172,15 +173,17 @@ AWS.NodeHttpClient = inherit({
172173
options.agent = new client.Agent(options);
173174
}
174175

175-
var req = this.setupEvents(client, options, request, response);
176-
if (request.httpRequest.body) {
177-
req.write(request.httpRequest.body);
176+
var stream = this.setupEvents(client, options, request, response);
177+
if (request.httpRequest.body instanceof Stream) {
178+
request.httpRequest.body.pipe(stream, {end: false});
179+
} else if (request.httpRequest.body) {
180+
stream.write(request.httpRequest.body);
178181
}
179-
req.end();
182+
stream.end();
180183
},
181184

182185
setupEvents: function setupEvents(client, options, request, response) {
183-
var req = client.request(options, function onResponse(httpResponse) {
186+
var stream = client.request(options, function onResponse(httpResponse) {
184187
request.emit('httpHeaders', httpResponse.statusCode,
185188
httpResponse.headers, response, request);
186189

@@ -193,12 +196,12 @@ AWS.NodeHttpClient = inherit({
193196
});
194197
});
195198

196-
req.on('error', function (err) {
199+
stream.on('error', function (err) {
197200
request.emit('httpError', AWS.util.error(err,
198201
{code: 'NetworkingError', retryable: true}));
199202
});
200203

201-
return req;
204+
return stream;
202205
}
203206
});
204207

lib/service_interface/json.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ AWS.ServiceInterface.Json = {
3131
var error = {};
3232
var httpResponse = resp.httpResponse;
3333

34-
if (httpResponse.body) {
35-
var e = JSON.parse(httpResponse.body);
34+
if (httpResponse.body.length > 0) {
35+
var e = JSON.parse(httpResponse.body.toString());
3636
error.code = e.__type.split('#').pop();
3737
if (error.code === 'RequestEntityTooLarge') {
3838
error.message = 'Request body must be less than 1 MB';
@@ -48,6 +48,6 @@ AWS.ServiceInterface.Json = {
4848
},
4949

5050
extractData: function extractData(resp) {
51-
resp.data = JSON.parse(resp.httpResponse.body || '{}');
51+
resp.data = JSON.parse(resp.httpResponse.body.toString() || '{}');
5252
}
5353
};

lib/service_interface/query.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ AWS.ServiceInterface.Query = {
3939
},
4040

4141
extractError: function extractError(resp) {
42-
var data = new AWS.XML.Parser({}).parse(resp.httpResponse.body);
42+
var data = new AWS.XML.Parser({}).parse(resp.httpResponse.body.toString());
4343
if (data.Code) {
4444
resp.error = AWS.util.error(new Error(), {
4545
code: data.Code,
@@ -57,7 +57,7 @@ AWS.ServiceInterface.Query = {
5757
var req = resp.request;
5858
var operation = req.client.api.operations[req.operation];
5959
var parser = new AWS.XML.Parser(operation.o || {});
60-
resp.data = parser.parse(resp.httpResponse.body);
60+
resp.data = parser.parse(resp.httpResponse.body.toString());
6161
}
6262
};
6363

lib/service_interface/rest_xml.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ AWS.ServiceInterface.RestXml = {
2626
extractError: function extractError(resp) {
2727
AWS.ServiceInterface.Rest.extractError(resp);
2828

29-
var data = new AWS.XML.Parser({}).parse(resp.httpResponse.body);
29+
var data = new AWS.XML.Parser({}).parse(resp.httpResponse.body.toString());
3030
if (data.Code) {
3131
resp.error = AWS.util.error(new Error(), {
3232
code: data.Code,
@@ -48,15 +48,15 @@ AWS.ServiceInterface.RestXml = {
4848
var operation = req.client.api.operations[req.operation];
4949
var rules = operation.o || {};
5050

51-
if (rules['Body'] && rules['Body']['t'] === 'bl') {
52-
resp.data['Body'] = httpResponse.body;
53-
} else if (httpResponse.body !== '') {
51+
if (rules.Body && rules.Body.t === 'bl') {
52+
resp.data.Body = httpResponse.body;
53+
} else if (httpResponse.body.length > 0) {
5454
var parser = new AWS.XML.Parser(rules);
55-
AWS.util.update(resp.data, parser.parse(httpResponse.body));
55+
AWS.util.update(resp.data, parser.parse(httpResponse.body.toString()));
5656
}
5757

5858
// extract request id
59-
resp.data['RequestId'] = httpResponse.headers['x-amz-request-id'];
59+
resp.data.RequestId = httpResponse.headers['x-amz-request-id'];
6060
},
6161

6262
populateBody: function populateBody(req) {
@@ -88,6 +88,16 @@ AWS.ServiceInterface.RestXml = {
8888
}
8989

9090
req.httpRequest.body = body;
91-
req.httpRequest.headers['Content-Length'] = body ? body.length : 0;
91+
req.httpRequest.headers['Content-Length'] =
92+
AWS.ServiceInterface.RestXml.getBodyLength(body);
93+
},
94+
95+
getBodyLength: function getBodyLength(body) {
96+
if (!body) return 0;
97+
if (body.length) {
98+
return body.length;
99+
} else if (body.path) { // assume file system stream
100+
return require('fs').lstatSync(body.path).size;
101+
}
92102
}
93103
};

lib/services/ec2.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ AWS.EC2.Client = inherit(AWS.Client, {
5050
extractError: function extractError(resp) {
5151
// EC2 nests the error code and message deeper than other AWS Query services.
5252
var httpResponse = resp.httpResponse;
53-
var data = new AWS.XML.Parser({}).parse(httpResponse.body || '');
53+
var data = new AWS.XML.Parser({}).parse(httpResponse.body.toString() || '');
5454
if (data.Errors)
5555
resp.error = AWS.util.error(new Error(), {
5656
code: data.Errors.Error.Code,

lib/services/s3.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ AWS.S3.Client = inherit(AWS.Client, {
126126
var req = resp.request;
127127
var httpResponse = resp.httpResponse;
128128
if (req.operation === 'completeMultipartUpload' &&
129-
httpResponse.body.match('<Error>'))
129+
httpResponse.body.toString().match('<Error>'))
130130
return false;
131131
else
132132
return httpResponse.statusCode < 300;
@@ -156,7 +156,7 @@ AWS.S3.Client = inherit(AWS.Client, {
156156
var req = resp.request;
157157
if (req.operation === 'getBucketLocation') {
158158
/*jshint regexp:false*/
159-
var match = resp.httpResponse.body.match(/>(.+)<\/Location/);
159+
var match = resp.httpResponse.body.toString().match(/>(.+)<\/Location/);
160160
if (match) {
161161
delete resp.data['_'];
162162
resp.data.LocationConstraint = match[1];
@@ -182,7 +182,7 @@ AWS.S3.Client = inherit(AWS.Client, {
182182
message: null
183183
});
184184
} else {
185-
var data = new AWS.XML.Parser({}).parse(resp.httpResponse.body);
185+
var data = new AWS.XML.Parser({}).parse(resp.httpResponse.body.toString());
186186

187187
resp.error = AWS.util.error(new Error(), {
188188
code: data.Code || resp.httpResponse.statusCode,

test/helpers.coffee

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ MockClient = AWS.util.inherit AWS.Client,
4848
@config.region = 'mock-region'
4949
setupRequestListeners: (request) ->
5050
request.on 'extractData', (resp) ->
51-
resp.data = resp.httpResponse.body
51+
resp.data = resp.httpResponse.body.toString()
5252
request.on 'extractError', (resp) ->
5353
resp.error =
5454
code: resp.httpResponse.statusCode
@@ -68,7 +68,7 @@ mockHttpResponse = (status, headers, data) ->
6868
req.emit('httpHeaders', status, headers, resp)
6969
str = str instanceof Array ? str : [str]
7070
AWS.util.arrayEach data, (str) ->
71-
req.emit('httpData', str, resp)
71+
req.emit('httpData', new Buffer(str), resp)
7272
req.emit('httpDone', resp)
7373
else
7474
req.emit('httpError', status, resp)

test/unit/event_listeners.spec.coffee

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe 'AWS.EventListeners', ->
9595

9696
# register httpData event
9797
request = makeRequest()
98-
request.on('httpData', (chunk) -> calls.push(chunk))
98+
request.on('httpData', (chunk) -> calls.push(chunk.toString()))
9999
request.send()
100100

101101
expect(calls).toEqual(['FOO', 'BAR', 'BAZ', 'QUX'])
@@ -105,7 +105,7 @@ describe 'AWS.EventListeners', ->
105105
request.on('httpData', ->)
106106
response = request.send()
107107

108-
expect(response.httpResponse.body).toEqual('')
108+
expect(response.httpResponse.body.toString()).toEqual('')
109109

110110
describe 'retry', ->
111111
it 'retries a request with a set maximum retries', ->

test/unit/service_interface/json.spec.coffee

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe 'AWS.ServiceInterface.Json', ->
7575
describe 'extractError', ->
7676
extractError = (body) ->
7777
response.httpResponse.statusCode = 500
78-
response.httpResponse.body = body
78+
response.httpResponse.body = new Buffer(body)
7979
svc.extractError(response)
8080

8181
it 'removes prefixes from the error code', ->
@@ -91,7 +91,7 @@ describe 'AWS.ServiceInterface.Json', ->
9191
expect(response.data).toEqual(null)
9292

9393
it 'returns the status code when the body is blank', ->
94-
extractError null
94+
extractError ''
9595
expect(response.error instanceof Error).toBeTruthy()
9696
expect(response.error.code).toEqual(500)
9797
expect(response.error.message).toEqual(null)
@@ -125,7 +125,7 @@ describe 'AWS.ServiceInterface.Json', ->
125125
describe 'extractData', ->
126126
extractData = (body) ->
127127
response.httpResponse.statusCode = 200
128-
response.httpResponse.body = body
128+
response.httpResponse.body = new Buffer(body)
129129
svc.extractData(response)
130130

131131
it 'JSON parses http response bodies', ->
@@ -139,6 +139,6 @@ describe 'AWS.ServiceInterface.Json', ->
139139
expect(response.data).toEqual({})
140140

141141
it 'returns an empty object when the body is null', ->
142-
extractData null
142+
extractData ''
143143
expect(response.error).toEqual(null)
144144
expect(response.data).toEqual({})

test/unit/service_interface/query.spec.coffee

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ describe 'AWS.ServiceInterface.Query', ->
8181
</Error>
8282
"""
8383
response.httpResponse.statusCode = 400
84-
response.httpResponse.body = body
84+
response.httpResponse.body = new Buffer(body)
8585
svc.extractError(response)
8686

8787
it 'extracts the error code and message', ->
@@ -100,7 +100,7 @@ describe 'AWS.ServiceInterface.Query', ->
100100
describe 'extractData', ->
101101
extractData = (body) ->
102102
response.httpResponse.statusCode = 200
103-
response.httpResponse.body = body
103+
response.httpResponse.body = new Buffer(body)
104104
svc.extractData(response)
105105

106106
it 'parses the response using the operation output rules', ->

test/unit/service_interface/rest_xml.spec.coffee

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ describe 'AWS.ServiceInterface.RestXml', ->
188188
</Error>
189189
"""
190190
response.httpResponse.statusCode = 400
191-
response.httpResponse.body = body
191+
response.httpResponse.body = new Buffer(body)
192192
svc.extractError(response)
193193

194194
it 'extracts the error code and message', ->
@@ -208,7 +208,7 @@ describe 'AWS.ServiceInterface.RestXml', ->
208208
describe 'extractData', ->
209209
extractData = (body) ->
210210
response.httpResponse.statusCode = 200
211-
response.httpResponse.body = body
211+
response.httpResponse.body = new Buffer(body)
212212
svc.extractData(response)
213213

214214
it 'parses the xml body', ->
@@ -224,3 +224,9 @@ describe 'AWS.ServiceInterface.RestXml', ->
224224
</xml>
225225
"""
226226
expect(response.data).toEqual({Foo:'foo', Bar:['a', 'b', 'c']})
227+
228+
it 'sets Body to a Buffer object', ->
229+
operation.o = Body: t: 'bl'
230+
extractData 'Buffer data'
231+
expect(response.data.Body instanceof Buffer).toBeTruthy()
232+
expect(response.data.Body.toString()).toEqual('Buffer data')

0 commit comments

Comments
 (0)