diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cc8efc51..25886d7a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.26.8" + ".": "0.26.9" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 066eadf6..8115aed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.26.9 (2023-09-25) + +Full Changelog: [v0.26.8...v0.26.9](https://github.com/lithic-com/lithic-node/compare/v0.26.8...v0.26.9) + +### Features + +* **client:** handle retry-after with a date ([#213](https://github.com/lithic-com/lithic-node/issues/213)) ([53eb832](https://github.com/lithic-com/lithic-node/commit/53eb832e403bcd6ccf1820f66ecd47b44b8aad3f)) +* **package:** export a root error type ([#212](https://github.com/lithic-com/lithic-node/issues/212)) ([78f89c1](https://github.com/lithic-com/lithic-node/commit/78f89c1b8ff5bf7e521c178da7af8abb2b466963)) + + +### Documentation + +* **api.md:** add shared models ([#211](https://github.com/lithic-com/lithic-node/issues/211)) ([bd02f27](https://github.com/lithic-com/lithic-node/commit/bd02f27a3126ffa6ccaee90b71c7c0a5b3301af5)) +* **README:** fix variable names in some examples ([#209](https://github.com/lithic-com/lithic-node/issues/209)) ([4b28d0d](https://github.com/lithic-com/lithic-node/commit/4b28d0dcac8ab512eaff022608da00bdb74459d3)) + ## 0.26.8 (2023-09-20) Full Changelog: [v0.26.7...v0.26.8](https://github.com/lithic-com/lithic-node/compare/v0.26.7...v0.26.8) diff --git a/README.md b/README.md index 119bf870..2c49f079 100644 --- a/README.md +++ b/README.md @@ -211,9 +211,9 @@ const response = await lithic.cards.create({ type: 'SINGLE_USE' }).asResponse(); console.log(response.headers.get('X-My-Header')); console.log(response.statusText); // access the underlying Response object -const { data: cards, response: raw } = await lithic.cards.create({ type: 'SINGLE_USE' }).withResponse(); +const { data: card, response: raw } = await lithic.cards.create({ type: 'SINGLE_USE' }).withResponse(); console.log(raw.headers.get('X-My-Header')); -console.log(cards.token); +console.log(card.token); ``` ## Configuring an HTTP(S) Agent (e.g., for proxies) diff --git a/api.md b/api.md index 70166388..fbeeea47 100644 --- a/api.md +++ b/api.md @@ -8,6 +8,14 @@ Methods: - client.apiStatus() -> APIStatus +# Shared + +Types: + +- Address +- Carrier +- ShippingAddress + # Accounts Types: diff --git a/package.json b/package.json index 9294c468..1357fd08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lithic", - "version": "0.26.8", + "version": "0.26.9", "description": "Client library for the Lithic API", "author": "Lithic ", "types": "dist/index.d.ts", diff --git a/src/core.ts b/src/core.ts index 444da38e..0faab1cd 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,5 +1,11 @@ import { VERSION } from './version'; -import { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError } from './error'; +import { + LithicError, + APIError, + APIConnectionError, + APIConnectionTimeoutError, + APIUserAbortError, +} from './error'; import { kind as shimsKind, type Readable, @@ -433,7 +439,7 @@ export abstract class APIClient { if (value === null) { return `${encodeURIComponent(key)}=`; } - throw new Error( + throw new LithicError( `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`, ); }) @@ -496,32 +502,37 @@ export abstract class APIClient { retriesRemaining -= 1; // About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After - // - // TODO: we may want to handle the case where the header is using the http-date syntax: "Retry-After: ". - // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax for details. - const retryAfter = parseInt(responseHeaders?.['retry-after'] || ''); + let timeoutMillis: number | undefined; + const retryAfterHeader = responseHeaders?.['retry-after']; + if (retryAfterHeader) { + const timeoutSeconds = parseInt(retryAfterHeader); + if (!Number.isNaN(timeoutSeconds)) { + timeoutMillis = timeoutSeconds * 1000; + } else { + timeoutMillis = Date.parse(retryAfterHeader) - Date.now(); + } + } - const maxRetries = options.maxRetries ?? this.maxRetries; - const timeout = this.calculateRetryTimeoutSeconds(retriesRemaining, retryAfter, maxRetries) * 1000; - await sleep(timeout); + // If the API asks us to wait a certain amount of time (and it's a reasonable amount), + // just do what it says, but otherwise calculate a default + if ( + !timeoutMillis || + !Number.isInteger(timeoutMillis) || + timeoutMillis <= 0 || + timeoutMillis > 60 * 1000 + ) { + const maxRetries = options.maxRetries ?? this.maxRetries; + timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries); + } + await sleep(timeoutMillis); return this.makeRequest(options, retriesRemaining); } - private calculateRetryTimeoutSeconds( - retriesRemaining: number, - retryAfter: number, - maxRetries: number, - ): number { + private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number { const initialRetryDelay = 0.5; const maxRetryDelay = 2; - // If the API asks us to wait a certain amount of time (and it's a reasonable amount), - // just do what it says. - if (Number.isInteger(retryAfter) && retryAfter <= 60) { - return retryAfter; - } - const numRetries = maxRetries - retriesRemaining; // Apply exponential backoff, but not more than the max. @@ -530,7 +541,7 @@ export abstract class APIClient { // Apply some jitter, plus-or-minus half a second. const jitter = Math.random() - 0.5; - return sleepSeconds + jitter; + return (sleepSeconds + jitter) * 1000; } private getUserAgent(): string { @@ -592,7 +603,7 @@ export abstract class AbstractPage implements AsyncIterable { async getNextPage(): Promise { const nextInfo = this.nextPageInfo(); if (!nextInfo) { - throw new Error( + throw new LithicError( 'No next page expected; please check `.hasNextPage()` before calling `.getNextPage()`.', ); } @@ -918,10 +929,10 @@ export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve const validatePositiveInteger = (name: string, n: unknown): number => { if (typeof n !== 'number' || !Number.isInteger(n)) { - throw new Error(`${name} must be an integer`); + throw new LithicError(`${name} must be an integer`); } if (n < 0) { - throw new Error(`${name} must be a positive integer`); + throw new LithicError(`${name} must be a positive integer`); } return n; }; @@ -932,7 +943,7 @@ export const castToError = (err: any): Error => { }; export const ensurePresent = (value: T | null | undefined): T => { - if (value == null) throw new Error(`Expected a value to be given but received ${value} instead.`); + if (value == null) throw new LithicError(`Expected a value to be given but received ${value} instead.`); return value; }; @@ -955,14 +966,14 @@ export const coerceInteger = (value: unknown): number => { if (typeof value === 'number') return Math.round(value); if (typeof value === 'string') return parseInt(value, 10); - throw new Error(`Could not coerce ${value} (type: ${typeof value}) into a number`); + throw new LithicError(`Could not coerce ${value} (type: ${typeof value}) into a number`); }; export const coerceFloat = (value: unknown): number => { if (typeof value === 'number') return value; if (typeof value === 'string') return parseFloat(value); - throw new Error(`Could not coerce ${value} (type: ${typeof value}) into a number`); + throw new LithicError(`Could not coerce ${value} (type: ${typeof value}) into a number`); }; export const coerceBoolean = (value: unknown): boolean => { @@ -1066,5 +1077,5 @@ export const toBase64 = (str: string | null | undefined): string => { return btoa(str); } - throw new Error('Cannot generate b64 string; Expected `Buffer` or `btoa` to be defined'); + throw new LithicError('Cannot generate b64 string; Expected `Buffer` or `btoa` to be defined'); }; diff --git a/src/error.ts b/src/error.ts index d9b00ceb..8319954f 100644 --- a/src/error.ts +++ b/src/error.ts @@ -2,7 +2,9 @@ import { castToError, Headers } from './core'; -export class APIError extends Error { +export class LithicError extends Error {} + +export class APIError extends LithicError { readonly status: number | undefined; readonly headers: Headers | undefined; readonly error: Object | undefined; diff --git a/src/index.ts b/src/index.ts index 8c16034b..2d9011b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -113,7 +113,7 @@ export class Lithic extends Core.APIClient { ...opts }: ClientOptions = {}) { if (apiKey === undefined) { - throw new Error( + throw new Errors.LithicError( "The LITHIC_API_KEY environment variable is missing or empty; either provide it, or instantiate the Lithic client with an apiKey option, like new Lithic({ apiKey: 'my apiKey' }).", ); } @@ -187,6 +187,7 @@ export class Lithic extends Core.APIClient { static Lithic = this; + static LithicError = Errors.LithicError; static APIError = Errors.APIError; static APIConnectionError = Errors.APIConnectionError; static APIConnectionTimeoutError = Errors.APIConnectionTimeoutError; @@ -202,6 +203,7 @@ export class Lithic extends Core.APIClient { } export const { + LithicError, APIError, APIConnectionError, APIConnectionTimeoutError, diff --git a/src/version.ts b/src/version.ts index 042c37d3..c8390bc3 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.26.8'; // x-release-please-version +export const VERSION = '0.26.9'; // x-release-please-version