Skip to content

Commit

Permalink
Add AbortSignal support
Browse files Browse the repository at this point in the history
  • Loading branch information
fedyk committed Dec 17, 2024
1 parent 69ac616 commit da68c48
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 10 deletions.
4 changes: 3 additions & 1 deletion src/RequestSender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ export class RequestSender {
headers: Object.assign({}, headers),
body: requestData,
protocol: this._stripe.getApiField('protocol'),
signal: options.settings.signal,
};

authenticator(request)
Expand All @@ -604,7 +605,8 @@ export class RequestSender {
request.headers,
request.body,
request.protocol,
timeout
timeout,
request.signal
);

const requestStartTime = Date.now();
Expand Down
7 changes: 6 additions & 1 deletion src/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type StripeRequest = {
headers: RequestHeaders;
body: string;
protocol: string;
signal?: AbortSignal;
};
export type RequestAuthenticator = (request: StripeRequest) => Promise<void>;
export type RequestCallback = (
Expand Down Expand Up @@ -84,7 +85,11 @@ export type RequestOpts = {
settings: RequestSettings;
usage: Array<string>;
};
export type RequestSettings = {timeout?: number; maxNetworkRetries?: number};
export type RequestSettings = {
timeout?: number;
signal?: AbortSignal;
maxNetworkRetries?: number;
};
export type ResponseEvent = {
api_version?: string;
account?: string;
Expand Down
14 changes: 12 additions & 2 deletions src/net/FetchHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,24 @@ export class FetchHttpClient extends HttpClient implements HttpClientInterface {
fetchFn: typeof fetch
): FetchWithTimeout {
return async (url, init, timeout): Promise<Response> => {
let signal = init.signal;

// Use AbortController because AbortSignal.timeout() was added later in Node v17.3.0, v16.14.0
const abort = new AbortController();

// fallback to timeout
if (!signal) {
signal = abort.signal;
}

let timeoutId: NodeJS.Timeout | null = setTimeout(() => {
timeoutId = null;
abort.abort(HttpClient.makeTimeoutError());
}, timeout);
try {
return await fetchFn(url, {
...init,
signal: abort.signal,
signal,
});
} catch (err) {
// Some implementations, like node-fetch, do not respect the reason passed to AbortController.abort()
Expand Down Expand Up @@ -117,7 +125,8 @@ export class FetchHttpClient extends HttpClient implements HttpClientInterface {
headers: RequestHeaders,
requestData: string,
protocol: string,
timeout: number
timeout: number,
signal?: AbortSignal
): Promise<HttpClientResponseInterface> {
const isInsecureConnection = protocol === 'http';

Expand All @@ -143,6 +152,7 @@ export class FetchHttpClient extends HttpClient implements HttpClientInterface {
headers,
// @ts-ignore
body,
signal,
},
timeout
);
Expand Down
6 changes: 4 additions & 2 deletions src/net/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export interface HttpClientInterface {
headers: RequestHeaders,
requestData: string,
protocol: string,
timeout: number
timeout: number,
signal?: AbortSignal
) => Promise<HttpClientResponseInterface>;
}

Expand Down Expand Up @@ -50,7 +51,8 @@ export class HttpClient implements HttpClientInterface {
headers: RequestHeaders,
requestData: string,
protocol: string,
timeout: number
timeout: number,
signal?: AbortSignal
): Promise<HttpClientResponseInterface> {
throw new Error('makeRequest not implemented.');
}
Expand Down
4 changes: 3 additions & 1 deletion src/net/NodeHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export class NodeHttpClient extends HttpClient {
headers: RequestHeaders,
requestData: string,
protocol: string,
timeout: number
timeout: number,
signal?: AbortSignal
): Promise<HttpClientResponseInterface> {
const isInsecureConnection = protocol === 'http';

Expand All @@ -64,6 +65,7 @@ export class NodeHttpClient extends HttpClient {
agent,
headers,
ciphers: 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:!MD5',
signal,
});

req.setTimeout(timeout, () => {
Expand Down
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const OPTIONS_KEYS = [
'apiVersion',
'maxNetworkRetries',
'timeout',
'signal',
'host',
'authenticator',
'stripeContext',
Expand All @@ -26,6 +27,7 @@ const OPTIONS_KEYS = [

type Settings = {
timeout?: number;
signal?: AbortSignal;
maxNetworkRetries?: number;
};

Expand Down Expand Up @@ -189,6 +191,9 @@ export function getOptionsFromArgs(args: RequestArgs): Options {
if (Number.isInteger(params.timeout)) {
opts.settings.timeout = params.timeout as number;
}
if (params.signal) {
opts.settings.signal = params.signal as AbortSignal;
}
if (params.host) {
opts.host = params.host as string;
}
Expand Down
18 changes: 17 additions & 1 deletion test/net/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export const createHttpClientTestSuite = (createHttpClientFn, extraTestsFn) => {
options.headers || {},
options.requestData,
'http',
options.timeout || 1000
options.timeout || 1000,
options.signal
);
};

Expand All @@ -77,6 +78,21 @@ export const createHttpClientTestSuite = (createHttpClientFn, extraTestsFn) => {
}
});

it('rejects with a timeout error on abort signal', async () => {
setupNock().reply(200, 'hello, world!');

try {
const controller = new AbortController();

controller.abort();

await sendRequest({signal: controller.signal});
fail();
} catch (e) {
expect(e.code).to.be.equal('ETIMEDOUT');
}
});

it('forwards any error', async () => {
setupNock().replyWithError('sample error');

Expand Down
6 changes: 4 additions & 2 deletions test/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export const getStripeMockClient = (): StripeClient => {
headers: RequestHeaders,
requestData: string,
_protocol: string,
timeout: number
timeout: number,
signal: AbortSignal
): Promise<HttpClientResponseInterface> {
return super.makeRequest(
process.env.STRIPE_MOCK_HOST || 'localhost',
Expand All @@ -83,7 +84,8 @@ export const getStripeMockClient = (): StripeClient => {
headers,
requestData,
'http',
timeout
timeout,
signal
);
}
}
Expand Down
17 changes: 17 additions & 0 deletions test/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,23 @@ describe('utils', () => {
});
});

it('parses abort signal settings', () => {
const abort = new AbortController();
const args = [
{
signal: abort.signal,
},
];

expect(utils.getOptionsFromArgs(args)).to.deep.equal({
host: null,
headers: {},
settings: {
signal: abort.signal,
},
});
});

it('warns if the hash contains something that does not belong', (done) => {
const args = [
{foo: 'bar'},
Expand Down
5 changes: 5 additions & 0 deletions types/lib.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ declare module 'stripe' {
*/
timeout?: number;

/**
* Specify an AbortSignal for this request.
*/
signal?: AbortSignal;

/**
* Specify the host for this request.
*/
Expand Down
9 changes: 9 additions & 0 deletions types/test/typescriptTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ stripe = new Stripe('sk_test_123', {
const rid: string = acct.lastResponse.requestId;
}
}

const abort = new AbortController();

await stripe.checkout.sessions.create(
{},
{
signal: abort.signal,
}
);
})();

const Foo = Stripe.StripeResource.extend({
Expand Down

0 comments on commit da68c48

Please sign in to comment.