Skip to content

Commit

Permalink
Add an HttpClient interface and NodeHttpClient implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
dcr-stripe committed Aug 12, 2021
1 parent c85cb03 commit 1efb6fb
Show file tree
Hide file tree
Showing 10 changed files with 819 additions and 176 deletions.
318 changes: 153 additions & 165 deletions lib/StripeResource.js

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions lib/net/HttpClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

/* eslint-disable class-methods-use-this */

/**
* Encapsulates the logic for issuing a request to the Stripe API. This is an
* experimental interface and is not yet stable.
*/
class HttpClient {
makeRequest(
host,
port,
path,
method,
headers,
requestData,
protocol,
timeout
) {
throw new Error('makeRequest not implemented.');
}

/** Helper to make a consistent timeout error across implementations. */
static makeTimeoutError() {
const timeoutErr = new TypeError(HttpClient.TIMEOUT_ERROR_CODE);
timeoutErr.code = HttpClient.TIMEOUT_ERROR_CODE;
return timeoutErr;
}
}

HttpClient.TIMEOUT_ERROR_CODE = 'ETIMEDOUT';

HttpClient.Response = class {
constructor(statusCode, headers) {
this._statusCode = statusCode;
this._headers = headers;
}

getStatusCode() {
return this._statusCode;
}

getHeaders() {
return this._headers;
}

getRawResponse() {
throw new Error('getRawResponse not implemented.');
}

toStream(streamCompleteCallback) {
throw new Error('toStream not implemented.');
}

toJSON() {
throw new Error('toJSON not implemented.');
}
};

module.exports = HttpClient;
119 changes: 119 additions & 0 deletions lib/net/NodeHttpClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use strict';

const http = require('http');
const https = require('https');

const HttpClient = require('./HttpClient');

const defaultHttpAgent = new http.Agent({keepAlive: true});
const defaultHttpsAgent = new https.Agent({keepAlive: true});

/**
* HTTP client which uses the Node `http` and `https` packages to issue
* requests.`
*/
class NodeHttpClient extends HttpClient {
constructor(agent) {
super();
this._agent = agent;
}

makeRequest(
host,
port,
path,
method,
headers,
requestData,
protocol,
timeout
) {
const isInsecureConnection = protocol === 'http';

let agent = this._agent;
if (!agent) {
agent = isInsecureConnection ? defaultHttpAgent : defaultHttpsAgent;
}

const requestPromise = new Promise((resolve, reject) => {
const req = (isInsecureConnection ? http : https).request({
host: host,
port: port,
path,
method,
agent,
headers,
ciphers: 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:!MD5',
});

req.setTimeout(timeout, () => {
req.destroy(HttpClient.makeTimeoutError());
});

req.on('response', (res) => {
resolve(new NodeHttpResponse(res));
});

req.on('error', (error) => {
reject(error);
});

req.once('socket', (socket) => {
if (socket.connecting) {
socket.once(
isInsecureConnection ? 'connect' : 'secureConnect',
() => {
// Send payload; we're safe:
req.write(requestData);
req.end();
}
);
} else {
// we're already connected
req.write(requestData);
req.end();
}
});
});

return requestPromise;
}
}

class NodeHttpResponse extends HttpClient.Response {
constructor(res) {
super(res.statusCode, res.headers || {});
this._res = res;
}

getRawResponse() {
return this._res;
}

toStream(streamCompleteCallback) {
this._res.once('end', () => streamCompleteCallback());
return this._res;
}

toJSON() {
return new Promise((resolve, reject) => {
let response = '';

this._res.setEncoding('utf8');
this._res.on('data', (chunk) => {
response += chunk;
});
this._res.once('end', () => {
try {
resolve(JSON.parse(response));
} catch (e) {
reject(e);
}
});
});
}
}

NodeHttpClient.Response = NodeHttpResponse;

module.exports = NodeHttpClient;
9 changes: 8 additions & 1 deletion lib/stripe.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const ALLOWED_CONFIG_PROPERTIES = [
'typescript',
'maxNetworkRetries',
'httpAgent',
'httpClient',
'timeout',
'host',
'port',
Expand All @@ -49,6 +50,9 @@ const {emitWarning} = utils;
Stripe.StripeResource = require('./StripeResource');
Stripe.resources = resources;

Stripe.HttpClient = require('./net/HttpClient');
Stripe.NodeHttpClient = require('./net/NodeHttpClient');

function Stripe(key, config = {}) {
if (!(this instanceof Stripe)) {
return new Stripe(key, config);
Expand Down Expand Up @@ -79,6 +83,8 @@ function Stripe(key, config = {}) {
);
}

const agent = props.httpAgent || null;

this._api = {
auth: null,
host: props.host || DEFAULT_HOST,
Expand All @@ -92,7 +98,8 @@ function Stripe(key, config = {}) {
props.maxNetworkRetries,
0
),
agent: props.httpAgent || null,
agent: agent,
httpClient: props.httpClient || new Stripe.NodeHttpClient(agent),
dev: false,
stripeAccount: props.stripeAccount || null,
};
Expand Down
Loading

0 comments on commit 1efb6fb

Please sign in to comment.