Retries and timeouts
The SDK retries reads that fail in a way that may pass, and never repeats a write. It has no timeout until you set one.
What is retried
Only GET and HEAD requests are retried, up to maxRetries times (default 2; 0 turns retries off), when:
- the request fails with a network error,
- an attempt times out (see Timeouts), or
- the API answers
429,502,503or504.
Before a retry the SDK waits. If the answer has a Retry-After header (seconds or an HTTP date) it waits that long. Otherwise it waits 250 ms for the first retry, 500 ms for the second, and so on, doubling each time. maxRetryDelayMs (default 8000) caps the doubling.
After the last retry, the last answer is thrown as a PrismDraftError, or the last network error is thrown as it is.
What is returned instead
Some answers are handed back at once, so a 429 or 5xx can reach your code without a retry:
- A
Retry-Afterlonger thanmaxRetryDelayMs. The server asked for more than the SDK will wait. The SDK does not retry sooner than it was told, so the call fails with that answer. The daily cap on image style runs is one: itsRetry-Aftercan be hours. - A
429without a usableRetry-After. A plan quota (QUOTA_EXCEEDEDfor projects, seats or storage) does not clear by waiting, and it sends noRetry-After. ARetry-Afterof 0 counts as none. - A write. A
POST,PUT,PATCHorDELETEthat reached the server may have been applied, so it is not sent again, even after a503. - A call you cancelled with
signal. The abort also ends a back-off wait at once.
The API's rate limit
The API allows 300 requests per minute per key by default. Past that it answers 429 with the code QUOTA_EXCEEDED and a Retry-After header of at most 60 seconds, the time left in the window.
With the default maxRetryDelayMs of 8000, a 429 whose window ends in more than 8 seconds is not retried: the call fails and you decide. To have the SDK wait out the whole window, raise the cap:
import { PrismDraft } from "@prism-draft/sdk";
const patient = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY!, maxRetries: 5 });
// Wait out the API's 60 s rate-limit window instead of failing with the 429:
const persistent = new PrismDraft({
apiKey: process.env.PRISMDRAFT_API_KEY!,
maxRetryDelayMs: 60_000,
});
A retry can then wait up to 60 seconds, so size maxRetries and your own deadlines with that in mind.
A write is not retried, so a 429 on a write is thrown. The rate limiter answers before the route runs, so nothing was written; its answer has the message Too many requests, slow down and no details. You can send the write again after waiting. Recipes paces a bulk update with this.
Timeouts
There is no timeout unless you set one: a stalled connection waits until you give up. timeoutMs bounds each attempt. Set it for the whole client, or per call, where it overrides the client's:
import { PrismDraft, PrismDraftTimeoutError } from "@prism-draft/sdk";
const pd = new PrismDraft({ apiKey: process.env.PRISMDRAFT_API_KEY!, timeoutMs: 10_000 });
try {
await pd.articles.get({ articleId });
await pd.articles.list({ workspaceId }, { timeoutMs: 60_000 }); // this call may take longer
} catch (error) {
if (error instanceof PrismDraftTimeoutError) {
console.log(`no answer within ${error.timeoutMs} ms`);
} else {
throw error;
}
}
timeoutMs is a finite number of milliseconds above 0; anything else throws a TypeError.
- It is per attempt. A read that is retried gets a fresh timer for each attempt, so a call can take up to
(maxRetries + 1) * timeoutMsplus the waits between attempts. - It covers the body. The timer runs while the request body is sent and the response body is read, not only until the headers arrive. Give a large upload more time with
init.timeoutMs. - A timed-out read is retried like a network error, up to
maxRetries. A timed-out write is not, because it may have landed. - The error is a
PrismDraftTimeoutError, anErrorwith atimeoutMsfield. It is not aPrismDraftError: the API never answered.
Cancellation
signal cancels a call from your side. You can pass both a signal and a timeoutMs: whichever comes first ends the call.
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
await pd.articles.get({ articleId }, { signal: controller.signal });
A call you cancelled rejects with your signal's reason (an AbortError by default), not with a timeout error, and is not retried. If the signal is already aborted, no request is sent.
A custom fetch you pass to the constructor must honour the request's signal, or neither timeouts nor cancellation can stop it.
A custom fetch and requestInit
fetch takes any function of the form (input: Request | URL | string, init?: RequestInit) => Promise<Response>, so a plain wrapper needs no cast, and under Bun's types it does not have to carry preconnect. The SDK calls it with a Request.
requestInit is a RequestInit that the SDK passes as the second argument of every fetch call, retries included. It is for options the runtime's fetch understands, such as cache: "no-store" under Next.js, whose patched fetch merges init into a Request. body, method, headers and signal belong to the client: the type omits them, and one that is passed anyway is dropped.
import { PrismDraft } from "@prism-draft/sdk";
const pd = new PrismDraft({
apiKey: process.env.PRISMDRAFT_API_KEY!,
requestInit: { cache: "no-store" },
fetch: (input, init) => fetch(input, init),
});