Skip to content

Commit

Permalink
Binary streaming
Browse files Browse the repository at this point in the history
  • Loading branch information
richardm-stripe committed Jun 21, 2021
1 parent 9b58b95 commit 1fc738d
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 43 deletions.
112 changes: 72 additions & 40 deletions lib/StripeResource.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,71 @@ StripeResource.prototype = {
};
},

_responseHandler(req, callback) {
_addHeadersDirectlyToResponse(res, headers) {
// For convenience, make some headers easily accessible on
// lastResponse.
res.requestId = headers['request-id'];

const stripeAccount = headers['stripe-account'];
if (stripeAccount) {
res.stripeAccount = stripeAccount;
}

const apiVersion = headers['stripe-version'];
if (apiVersion) {
res.apiVersion = apiVersion;
}

const idempotencyKey = headers['idempotency-key'];
if (idempotencyKey) {
res.idempotencyKey = idempotencyKey;
}
},

_makeResponseEvent(req, res, headers) {
const requestEndTime = Date.now();
const requestDurationMs = requestEndTime - req._requestStart;

return utils.removeNullish({
api_version: headers['stripe-version'],
account: headers['stripe-account'],
idempotency_key: headers['idempotency-key'],
method: req._requestEvent.method,
path: req._requestEvent.path,
status: res.statusCode,
request_id: res.requestId,
elapsed: requestDurationMs,
request_start_time: req._requestStart,
request_end_time: requestEndTime,
});
},

/**
* Used by methods with spec.binary === true. For these methods, we do not buffer the
* response into memory, or do any parsing into stripe objects or errors, we
* delegate that all of that to the user and pass back the raw http.Response
* object to the callback.
*/
_binaryResponseHandler(req, callback) {
return (res) => {
const headers = res.headers || {};
this._addHeadersDirectlyToResponse(res, headers);

res.once('end', () => {
const responseEvent = this._makeResponseEvent(req, res, headers);
this._stripe._emitter.emit('response', responseEvent);
this._recordRequestMetrics(res.requestId, responseEvent.elapsed);
});
return callback(null, res);
};
},

/**
* Default handler for Stripe responses. Buffers the response into memory,
* parses the JSON and returns it (i.e. passes it to the callback) if there
* is no "error" field. Otherwise constructs/passes an appropriate Error.
*/
_jsonResponseHandler(req, callback) {
return (res) => {
let response = '';

Expand All @@ -112,43 +176,8 @@ StripeResource.prototype = {
});
res.once('end', () => {
const headers = res.headers || {};
// NOTE: Stripe responds with lowercase header names/keys.

// For convenience, make some headers easily accessible on
// lastResponse.
res.requestId = headers['request-id'];

const stripeAccount = headers['stripe-account'];
if (stripeAccount) {
res.stripeAccount = stripeAccount;
}

const apiVersion = headers['stripe-version'];
if (apiVersion) {
res.apiVersion = apiVersion;
}

const idempotencyKey = headers['idempotency-key'];
if (idempotencyKey) {
res.idempotencyKey = idempotencyKey;
}

const requestEndTime = Date.now();
const requestDurationMs = requestEndTime - req._requestStart;

const responseEvent = utils.removeNullish({
api_version: headers['stripe-version'],
account: headers['stripe-account'],
idempotency_key: headers['idempotency-key'],
method: req._requestEvent.method,
path: req._requestEvent.path,
status: res.statusCode,
request_id: res.requestId,
elapsed: requestDurationMs,
request_start_time: req._requestStart,
request_end_time: requestEndTime,
});

this._addHeadersDirectlyToResponse(res, headers);
const responseEvent = this._makeResponseEvent(req, res, headers);
this._stripe._emitter.emit('response', responseEvent);

try {
Expand Down Expand Up @@ -194,7 +223,7 @@ StripeResource.prototype = {
);
}

this._recordRequestMetrics(res.requestId, requestDurationMs);
this._recordRequestMetrics(res.requestId, responseEvent.elapsed);

// Expose res object
Object.defineProperty(response, 'lastResponse', {
Expand Down Expand Up @@ -458,7 +487,10 @@ StripeResource.prototype = {
((res || {}).headers || {})['retry-after']
);
} else {
return this._responseHandler(req, callback)(res);
if (options.binary && res.statusCode <= 400) {
return this._binaryResponseHandler(req, callback)(res);
}
return this._jsonResponseHandler(req, callback)(res);
}
});

Expand Down
7 changes: 4 additions & 3 deletions lib/makeRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ function getRequestOpts(self, requestArgs, spec, overrideData) {
const requestMethod = (spec.method || 'GET').toUpperCase();
const urlParams = spec.urlParams || [];
const encode = spec.encode || ((data) => data);
const host = spec.host;
const path = self.createResourcePathWithSymbols(spec.path);

// Don't mutate args externally.
Expand All @@ -31,7 +30,8 @@ function getRequestOpts(self, requestArgs, spec, overrideData) {
const dataFromArgs = utils.getDataFromArgs(args);
const data = encode(Object.assign({}, dataFromArgs, overrideData));
const options = utils.getOptionsFromArgs(args);

const host = options.host || spec.host;
const binary = !!spec.binary;
// Validate that there are no more args.
if (args.filter((x) => x != null).length) {
throw new Error(
Expand All @@ -58,6 +58,7 @@ function getRequestOpts(self, requestArgs, spec, overrideData) {
auth: options.auth,
headers,
host,
binary,
settings: options.settings,
};
}
Expand Down Expand Up @@ -99,7 +100,7 @@ function makeRequest(self, requestArgs, spec, overrideData) {
path,
opts.bodyData,
opts.auth,
{headers, settings},
{headers, settings, binary: opts.binary},
requestCallback
);
});
Expand Down
4 changes: 4 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const OPTIONS_KEYS = [
'apiVersion',
'maxNetworkRetries',
'timeout',
'host',
];

const DEPRECATED_OPTIONS = {
Expand Down Expand Up @@ -197,6 +198,9 @@ const utils = (module.exports = {
if (Number.isInteger(params.timeout)) {
opts.settings.timeout = params.timeout;
}
if (params.host) {
opts.host = params.host;
}
}
}
return opts;
Expand Down
82 changes: 82 additions & 0 deletions test/StripeResource.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const nock = require('nock');

const stripe = require('../testUtils').getSpyableStripe();
const expect = require('chai').expect;
const testUtils = require('../testUtils');

const StripeResource = require('../lib/StripeResource');
const stripeMethod = StripeResource.method;

describe('StripeResource', () => {
describe('createResourcePathWithSymbols', () => {
Expand Down Expand Up @@ -635,4 +639,82 @@ describe('StripeResource', () => {
done();
});
});

describe('binary streaming', () => {
/**
* Defines a fake resource with a `pdf` method
* with binary streaming enabled.
*/
const makeResourceWithPDFMethod = (stripe) => {
return new (StripeResource.extend({
path: 'resourceWithPDF',

pdf: stripeMethod({
method: 'GET',
host: 'files.stripe.com',
binary: true,
}),
}))(stripe);
};

it('success', (callback) => {
const handleRequest = (req, res) => {
setTimeout(() => res.write('pretend'), 10);
setTimeout(() => res.write(' this'), 20);
setTimeout(() => res.write(' is a pdf'), 30);
setTimeout(() => res.end(), 40);
};

testUtils.getTestServerStripe({}, handleRequest, (err, stripe) => {
const foos = makeResourceWithPDFMethod(stripe);
if (err) {
return callback(err);
}

return foos.pdf({id: 'foo_123'}, {host: 'localhost'}, (err, res) => {
if (err) {
return callback(err);
}
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('error', callback);
res.on('end', () => {
expect(Buffer.concat(chunks).toString()).to.equal(
'pretend this is a pdf'
);
return callback();
});
});
});
});

it('failure', (callback) => {
const handleRequest = (req, res) => {
setTimeout(() => res.writeHead(500));
setTimeout(
() =>
res.write(
'{"error": "api_error", "error_description": "this is bad"}'
),
10
);
setTimeout(() => res.end(), 20);
};

testUtils.getTestServerStripe({}, handleRequest, (err, stripe) => {
if (err) {
return callback(err);
}

const foos = makeResourceWithPDFMethod(stripe);

return foos.pdf({id: 'foo_123'}, {host: 'localhost'}, (err, res) => {
expect(err).to.exist;
expect(err.raw.type).to.equal('api_error');
expect(err.raw.message).to.equal('this is bad');
return callback();
});
});
});
});
});

0 comments on commit 1fc738d

Please sign in to comment.