Skip to content

Retries and idempotency

createRetryingFetch() retries a request after a network error, a timeout, 408, 429 or a 5xx, with exponential backoff, and waits as long as Retry-After asks. Every POST and DELETE gets an Idempotency-Key, the same on each retry, so a retried request never runs twice.

Every method also takes RequestOptions as its last argument: your own idempotencyKey, extra headers (such as MoreVoice-Version) and an abort signal.

morevoice.ts
// Retries on network errors, timeouts, 408, 429 and 5xx, honouring Retry-After.
// POST and DELETE carry an Idempotency-Key, so a retry never runs twice.
import { createClient, createResources, createRetryingFetch } from "@morevoice/sdk";
const mv = createResources({
client: createClient({
baseUrl: "https://api.morevoice.ai/v1",
auth: process.env.MOREVOICE_API_KEY,
fetch: createRetryingFetch({
maxRetries: 3,
onRetry: (info) => console.warn(`Retry ${info.attempt} in ${info.delayMs} ms: ${info.method} ${info.url}`),
}),
}),
});
export default mv;

Exported from @morevoice/sdk (source: packages/sdk-ts/src/lib/request.ts).

constant
const IDEMPOTENCY_HEADER = "Idempotency-Key";
constant
const DEFAULT_MAX_RETRIES = 2;
interface
interface RetryInfo {
/** 1 for the first retry. */
attempt: number;
/** How long it waits before this retry. */
delayMs: number;
method: string;
url: string;
/** The status that caused the retry, when there was a response. */
status?: number;
/** The network error or timeout that caused the retry. */
error?: unknown;
idempotencyKey?: string;
}
interface
interface RetryingFetchOptions {
/** The underlying fetch (default: the global one, resolved per request). */
fetch?: FetchLike;
/** Retries after the first attempt (default 2; 0 disables). */
maxRetries?: number;
/** Per-attempt time limit until the response headers arrive; a timeout is retried like a network error. */
timeoutMs?: number;
/** First backoff (default 500 ms), doubled per retry. */
initialDelayMs?: number;
/** Backoff ceiling (default 8 s). */
maxDelayMs?: number;
/** Longest Retry-After / RateLimit wait honoured (default 60 s); a longer one returns the response instead. */
maxRetryAfterMs?: number;
/** Idempotency-Key generator (default uuidv7). */
idempotencyKey?: () => string;
/** Observe retries (logging, metrics). */
onRetry?: (info: RetryInfo) => void;
/** Jitter source (tests). */
random?: () => number;
}
function

RFC 9562 UUIDv7: 48-bit Unix-ms timestamp + 74 random bits, so keys sort by creation time.

function uuidv7(now: number = Date.now()): string;
function

Whether a response is worth retrying (doc 07 §7.1; WS05 v1 idempotency and rate limits).

function isRetryableResponse(response: Response): Promise<boolean>;
function

A fetch with idempotency keys and retries, for the generated client. Same signature as fetch; resolves with the final response (successful or not, the generated client maps errors) or rejects with the last network error.

function createRetryingFetch(options: RetryingFetchOptions = {}): FetchLike;
type

The fetch signature the helpers call: the global fetch, or any compatible implementation (undici, a mock…).

type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
function

Milliseconds until the exhausted rate-limit window resets, from the RateLimit headers: the longest t among policies with nothing remaining (or, when none says so, the longest t). Undefined when absent.

function rateLimitResetMs(headers: HeaderGetter): number | undefined;
function

Milliseconds from Retry-After, or undefined when absent / unparseable.

function retryAfterMs(headers: HeaderGetter, now: number = Date.now()): number | undefined;

Exported from @morevoice/sdk (source: packages/sdk-ts/src/facade-runtime.ts).

interface

Per-request options every facade method accepts as its last argument.

interface RequestOptions {
/**
* Sent as `Idempotency-Key` (POST and DELETE). Reuse it to retry a request safely for 24 hours: a retried
* `calls.create` never dials twice. When omitted, the client's retry layer generates one per call (WS06-W1-07).
*/
idempotencyKey?: string;
/** Extra request headers, e.g. `MoreVoice-Version` or an operation's own header parameters. */
headers?: Record<string, string>;
/** Aborts the request (and, for lists and streams, stops fetching further pages or events). */
signal?: AbortSignal;
}