From 3da0785acfa5998807fd6cec58364a9225272478 Mon Sep 17 00:00:00 2001 From: "gocardless-ci-robot[bot]" <123969075+gocardless-ci-robot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:48:27 +0000 Subject: [PATCH] Changes generated by 811a5c904f03659e41505c41b790f52df39fadd3 This commit was automatically created from gocardless/client-library-templates@811a5c904f03659e41505c41b790f52df39fadd3 by the `push-files` action. Workflow run: https://github.com/gocardless/client-library-templates/actions/runs/40309967060 --- README.md | 1 + package-lock.json | 50 ++++++++++++++++++++++----------------------- src/api/api.test.ts | 47 +++++++++++++++++++++++++++++++++++++++++- src/api/api.ts | 14 ++++++++++++- 4 files changed, 85 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index cc0f52fb..32c05a50 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ for await (const payment of client.payments.all()) { ### Available client options - `raiseOnIdempotencyConflict`: set to `true` to raise exceptions on [idempotency](https://developer.gocardless.com/api-reference/#making-requests-idempotency-keys) conflicts. Defaults to `false`. +- `timeout`: how long to wait for a response, in milliseconds, before aborting the request with a `TimeoutError` (from [`got`](https://github.com/sindresorhus/got/blob/main/documentation/6-timeout.md), which this library uses to make requests). A number applies to the whole request; an object such as `{ connect: 1000, response: 5000 }` sets limits for individual phases of the request. Defaults to 60000 (60 seconds); set it to `0` to disable the timeout. ### CommonJS backwards compatibility diff --git a/package-lock.json b/package-lock.json index 242c7adb..6151c27b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -196,9 +196,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", "dev": true, "license": "MIT", "engines": { @@ -240,13 +240,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", - "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.8" + "@babel/types": "^7.29.11" }, "bin": { "parser": "bin/babel-parser.js" @@ -529,13 +529,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-string-parser": "^7.29.11", "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { @@ -1396,9 +1396,9 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", - "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz", + "integrity": "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==", "dev": true, "license": "MIT", "optional": true, @@ -1413,8 +1413,8 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", + "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" } }, "node_modules/@parcel/watcher": { @@ -2761,9 +2761,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.11.31", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.31.tgz", - "integrity": "sha512-b0k7wXlWjpuL/Uxb/pYlpjY0ej5BjAmurtPUk3eRGuloI4yjK32m8TiKUQ81l1ptcLjKS/KH2+uqIolZti1x4Q==", + "version": "2.11.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.33.tgz", + "integrity": "sha512-yiVKDX+qCMRKBE084lXvq9EcwFUhKZ2nU8TK1EQQuOgPNR/lmbGxXOeDdlGUqC2Zz11Cs9rk1d/8NfgBmlkt0w==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3211,9 +3211,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.463", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.463.tgz", - "integrity": "sha512-nVWNU6ltFjopvOdD44J4zoBkgO0M+WZiFKKFV7EjebOH9z0KNphk6bCEjplXtJofL21ARIuuwWOCsFkBVklm6w==", + "version": "1.5.464", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", + "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", "dev": true, "license": "ISC" }, @@ -4071,9 +4071,9 @@ } }, "node_modules/ignore": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz", - "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", "dev": true, "license": "MIT", "engines": { diff --git a/src/api/api.test.ts b/src/api/api.test.ts index e8088621..655037da 100644 --- a/src/api/api.test.ts +++ b/src/api/api.test.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import nock from 'nock'; -import { Api } from './api'; +import { Api, DEFAULT_TIMEOUT_MS } from './api'; import { GoCardlessClient } from '../client'; import { Environments } from '../constants'; import * as GoCardlessErrors from '../errors'; @@ -79,6 +79,51 @@ describe('.request', () => { }); }); + describe('timeout option', () => { + const requestOptions = (options = {}) => + new Api(token, Environments.Live, options)['createRequestOptions']('post', {}); + + test('applies a default timeout when none is configured', () => { + expect(DEFAULT_TIMEOUT_MS).toEqual(60000); + expect(requestOptions().timeout).toEqual({ request: DEFAULT_TIMEOUT_MS }); + }); + + test('a configured timeout replaces the default', () => { + expect(requestOptions({ timeout: 5000 }).timeout).toEqual({ request: 5000 }); + expect(requestOptions({ timeout: { connect: 1000 } }).timeout).toEqual({ connect: 1000 }); + }); + + test('a timeout of 0 disables the timeout', () => { + expect(requestOptions({ timeout: 0 }).timeout).toBeUndefined(); + }); + + // POST so that `got` does not retry the timed out request + const params = { path: '/', method: 'post', requestParameters: {}, fetch: null }; + + test('aborts the request when a numeric timeout is exceeded', async () => { + const api = new Api(token, Environments.Live, { timeout: 50 }); + nock('https://api.gocardless.com').post('/').delay(500).reply(200, {}); + + await expect(api.request(params)).rejects.toMatchObject({ name: 'TimeoutError' }); + }); + + test('aborts the request when a per-phase timeout is exceeded', async () => { + const api = new Api(token, Environments.Live, { timeout: { response: 50 } }); + nock('https://api.gocardless.com').post('/').delay(500).reply(200, {}); + + await expect(api.request(params)).rejects.toMatchObject({ name: 'TimeoutError' }); + }); + + test('does not affect requests that complete in time', async () => { + const api = new Api(token, Environments.Live, { timeout: 5000 }); + const check = nock('https://api.gocardless.com').post('/').reply(200, {}); + + await api.request(params); + + expect(check.isDone()).toEqual(true); + }); + }); + describe('API environments', () => { test('when in Sandbox environment', async () => { const check = nock('https://api-sandbox.gocardless.com').get('/').reply(200, {}); diff --git a/src/api/api.ts b/src/api/api.ts index 8007f8f4..63989f7f 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -2,7 +2,7 @@ import * as os from 'os'; import * as process from 'process'; import { v4 as uuidv4 } from 'uuid'; import * as url from 'url'; -import got, { Agents, HTTPError, OptionsOfJSONResponseBody, ParseError } from 'got'; +import got, { Agents, Delays, HTTPError, OptionsOfJSONResponseBody, ParseError } from 'got'; import * as qs from 'qs'; import { Environments, CLIENT_VERSION, API_VERSION } from '../constants.js'; @@ -13,8 +13,11 @@ import { ApiRequestSigningOptionsInternal, } from '../apiRequestSigning.js'; +export const DEFAULT_TIMEOUT_MS = 60000; + export interface APIOptions { proxy?: Agents; + timeout?: number | Delays; raiseOnIdempotencyConflict?: boolean; apiRequestSigningOptions?: ApiRequestSigningOptions; } @@ -67,6 +70,7 @@ export class Api { private _environment: Environments; private _baseUrl: string; private _agent: Agents | undefined; + private _timeout: Delays | undefined; private raiseOnIdempotencyConflict: boolean; private apiRequestSigningOptions: ApiRequestSigningOptionsInternal | null = null; @@ -88,6 +92,13 @@ export class Api { this._agent = options.proxy; } + const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS; + if (typeof timeout === 'number') { + this._timeout = timeout > 0 ? { request: timeout } : undefined; + } else { + this._timeout = timeout; + } + this.raiseOnIdempotencyConflict = options.raiseOnIdempotencyConflict || false; this.apiRequestSigningOptions = options.apiRequestSigningOptions as ApiRequestSigningOptionsInternal; @@ -222,6 +233,7 @@ export class Api { const json = this.getRequestBody(method, requestParameters, payloadKey); return { agent: this._agent, + timeout: this._timeout, prefixUrl: this._baseUrl, // `got` v16 lets an absolute URL replace `prefixUrl`, which would send the token to another host allowAbsoluteUrls: false,