Skip to content
GigadriveDocs

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

ClassExtendsExtra fieldsRaised when
GigadriveErrorErrornonebase class, never thrown directly
AuthenticationErrorGigadriveErrornoneno credential at construction, a token exchange or refresh failed, or a request was still 401 after a refresh
ConfigurationErrorGigadriveErrornoneno application context, or a bucket reference that is missing or doubled
ApiErrorGigadriveErrorstatus: number, code?: stringany non-2xx response from the API
UploadErrorGigadriveErrorcause: unknowna byte transfer failed, or a session ended failed, expired or timed out
UploadSessionExpiredErrorUploadErrorcause: unknownthe 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.

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 fieldsEndpoints
page, perPage, cursororganizations, members, products, environment variables, applications, storage buckets, upload sessions
cursor, limitstorage objects, storage trash, application requests, AI Gateway usage requests
offset, limit, createdAt[gt]deployment logs
nonedeployments.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.