Retries and timeouts

odnoga handles vendor-side retries and fallback. You still need to handle network / 429 / 5xx on your side — but only those.

Rules

  • Never retry 4xx other than 408 and 429. They are deterministic; retrying just wastes money.
  • Always retry network errors, 408, 429, 5xx. Exponential backoff, jitter, cap at 3 attempts.
  • Honor x-ratelimit-reset if present — wait until then before retrying.
  • Set a hard request timeout (e.g. 60s). Don't rely on the platform.

Helper

import { callodnoga, AirouterError } from './airouter.ts';

const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);

export async function callWithRetry<T>(
  path: string, body: unknown, opts: { endUser: string; timeoutMs?: number } = { endUser: 'anonymous' },
) {
  const timeoutMs = opts.timeoutMs ?? 60_000;
  let lastErr: unknown;

  for (let attempt = 0; attempt < 3; attempt++) {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort(), timeoutMs);
    try {
      return await callodnoga<T>(path, body, { endUser: opts.endUser, signal: ctrl.signal });
    } catch (e) {
      lastErr = e;
      const status = e instanceof AirouterError ? e.status : 0;
      const retryable = status === 0 || RETRYABLE.has(status);
      if (!retryable) throw e;
      const backoff = Math.min(8000, 250 * 2 ** attempt) + Math.random() * 250;
      await new Promise((r) => setTimeout(r, backoff));
    } finally {
      clearTimeout(t);
    }
  }
  throw lastErr;
}

Idempotency

Streaming or write-style requests should send x-airouter-idempotency-key (any unique string per logical call). Retries with the same key are deduped server-side, so two attempts of the same request bill once.