Errors and pagination
The error classes the SDK throws, what it retries for you, and how to walk a list endpoint to the end.
Every error the Gigadrive Network SDK raises extends GigadriveError, so one instanceof check separates SDK failures from bugs in your own code. Most list endpoints share one response shape, and one helper walks it.
Error classes
| Class | Extends | Extra fields | Raised when |
|---|---|---|---|
GigadriveError | Error | none | base class, never thrown directly |
AuthenticationError | GigadriveError | none | no credential at construction, a token exchange or refresh failed, or a request was still 401 after a refresh |
ConfigurationError | GigadriveError | none | no application context, or a bucket reference that is missing or doubled |
ApiError | GigadriveError | status: number, code?: string | any non-2xx response from the API |
UploadError | GigadriveError | cause: unknown | a byte transfer failed, or a session ended failed, expired or timed out |
UploadSessionExpiredError | UploadError | cause: unknown | the upload URL returned 401, 403 or 410, so the session is gone |
Each constructor sets name to the class name, so err.name and instanceof agree.
import { ApiError, AuthenticationError, ConfigurationError } from '@gigadrive/sdk';
try {
await client.storage.objects.list('invoices', { prefix: '2026/' });
} catch (error) {
if (error instanceof ConfigurationError) {
// No application context: set applicationId on the client.
throw error;
}
if (error instanceof AuthenticationError) {
// The credential is wrong, revoked, or expired.
throw error;
}
if (error instanceof ApiError && error.status === 404) {
return [];
}
throw error;
}ApiError.message is whatever the API put in the response body's error field, falling back to the HTTP status text when the body is missing or unparseable. code is populated only when the API returns error as an object, which most endpoints do not, so branch on status rather than on code.
Not everything is an SDK class. Invalid upload sources, a partNumber below 1, a missing ETag on a deployment part, a malformed server-sent event, and cancellation all throw a plain Error. Cancellation is identified by error.name === 'AbortError'.
What the client retries
One thing: a 401. The client invalidates the cached token, fetches a new one, and replays the request once. A second 401 throws AuthenticationError('Authentication failed after token refresh'). A request whose body is a ReadableStream is never replayed, because the stream is already consumed.
Nothing else is retried. A 429 or a 502 comes straight back to you as an ApiError, and the backoff is yours to write.
Rate limits surface as an opaque message
A throttled request comes back with error set to rate_limit_exceeded, so the SDK reports err.status === 429 and
err.message === 'rate_limit_exceeded'. The readable sentence sits in a message field the SDK does not read. Wait
60 seconds, the length of the counting window, before trying again. Rate limits
lists the budgets.
import { ApiError } from '@gigadrive/sdk';
async function withRetry<T>(operation: () => Promise<T>, attempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await operation();
} catch (error) {
const retryable = error instanceof ApiError && (error.status === 429 || error.status >= 500);
if (!retryable || attempt >= attempts) throw error;
await new Promise((resolve) => setTimeout(resolve, attempt * 60_000));
}
}
}
const deployment = await withRetry(() => client.deployments.create({ applicationId }));File uploads are the exception to all of this: the resumable transport retries the byte transfer on its own schedule, described in Uploading files.
Pagination
Nearly every list endpoint returns one shape:
interface Paginated<T> {
items: T[];
total: number;
nextCursor?: string;
}total counts every matching record, including records absent from the current response. nextCursor appears on cursor-paginated endpoints while more records remain, and its absence is what ends iteration.
paginate turns any of them into an async iterable. Give it a function that fetches one page for a cursor:
import { paginate } from '@gigadrive/sdk';
for await (const object of paginate((cursor) => client.storage.objects.list('invoices', { cursor }))) {
console.log(object.key, object.contentLength);
}It follows nextCursor and stops when there is none. It ignores page, perPage and total, so pass it a call that accepts a cursor.
Which style each endpoint takes:
| Query fields | Endpoints |
|---|---|
page, perPage, cursor | organizations, members, products, environment variables, applications, storage buckets, upload sessions |
cursor, limit | storage objects, storage trash, application requests, AI Gateway usage requests |
offset, limit, createdAt[gt] | deployment logs |
| none | deployments.list, deployments.getHostnames, applications.hostnames, apiKeys.list, aiGateway.listModels, budgets.list |
A few responses sit outside the common shape. organizations.aiGateway.budgets.list and budgets.replace return { items } with no total, and aiGateway.videos.listModels returns { object, data }.
Deployment logs differ more. getLogs returns DeploymentLogPage, carrying totalItems, limit, offset and items, which is not a Paginated<T>, so paginate does not apply. Page it by advancing offset, or tail a running build by remembering the last timestamp you saw:
let since: string | undefined;
for (;;) {
const page = await client.deployments.getLogs(deployment.id, {
limit: 100,
...(since ? { 'createdAt[gt]': since } : {}),
});
for (const entry of page.items) {
console.log(`[${entry.type}] ${entry.message}`);
since = entry.createdAt;
}
const { status } = await client.deployments.get(deployment.id);
if (status === 'ACTIVE' || status === 'FAILED') break;
await new Promise((resolve) => setTimeout(resolve, 2_000));
}The full list of endpoints and the shapes they return is in Resources.
