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