> ## Documentation Index
> Fetch the complete documentation index at: https://docs.retriever.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Le client Node commun

> Client Node.js réutilisable pour l'API Retriever : gestion des erreurs, timeouts et retries sûrs sur 429 et 5xx.

Les recettes Node importent ce fichier `retriever.mjs`. Exportez d'abord vos variables :

```bash theme={null}
export RETRIEVER_API_URL="https://api.retriever.run"
export RETRIEVER_API_KEY="ret_live_..."
```

Il gère le format d'erreur, les timeouts et les retries **sûrs** :

* `429` : toujours réessayé, après `Retry-After`.
* `5xx` et erreurs réseau : réessayés seulement pour `GET`, ou pour un `POST` avec `Idempotency-Key`. Un `POST` sans clé n'est jamais rejoué, pour ne pas créer de doublon ni facturer deux fois.

```js theme={null}
// retriever.mjs — Node 18+
export const API = process.env.RETRIEVER_API_URL;
export const KEY = process.env.RETRIEVER_API_KEY;
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export class RetrieverError extends Error {
  constructor(status, code, message) {
    super(`${status} ${code}: ${message}`);
    Object.assign(this, { status, code });
  }
}

export async function call(path, { method = "GET", body, headers = {}, retries = 5, timeoutMs = 60_000 } = {}) {
  const canRetryFailure = method === "GET" || "Idempotency-Key" in headers;
  for (let attempt = 0; ; attempt++) {
    let res;
    try {
      res = await fetch(`${API}${path}`, {
        method,
        signal: AbortSignal.timeout(timeoutMs),
        headers: {
          Authorization: `Bearer ${KEY}`,
          ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
          ...headers,
        },
        body: body !== undefined ? JSON.stringify(body) : undefined,
      });
    } catch (networkError) {
      if (!canRetryFailure || attempt >= retries) throw networkError;
      await sleep(Math.min(30_000, 500 * 2 ** attempt));
      continue;
    }
    if (res.ok) return res.json();

    const payload = await res.json().catch(() => ({}));
    const error = payload.error ?? {};
    const retryable = res.status === 429 || (res.status >= 500 && canRetryFailure);
    if (!retryable || attempt >= retries) {
      throw new RetrieverError(res.status, error.code ?? `http_${res.status}`, error.message ?? res.statusText);
    }
    const retryAfter = Number(res.headers.get("retry-after"));
    await sleep(retryAfter > 0 ? retryAfter * 1000 : Math.min(30_000, 500 * 2 ** attempt));
  }
}

export async function download(path) {
  const res = await fetch(`${API}${path}`, { headers: { Authorization: `Bearer ${KEY}` } });
  if (!res.ok) throw new RetrieverError(res.status, `http_${res.status}`, "download failed");
  return Buffer.from(await res.arrayBuffer());
}

export async function waitRun(id, { timeoutMs = 60 * 60_000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  for (;;) {
    const run = await call(`/v1/runs/${id}`);
    if (!["queued", "running", "recovering"].includes(run.status)) return run;
    if (Date.now() > deadline) throw new Error(`Run ${id} toujours actif après ${timeoutMs} ms`);
    await sleep(15_000);
  }
}

export async function* paginateRows(rowsPath) {
  let cursor = null;
  do {
    const qs = new URLSearchParams({ limit: "1000", ...(cursor ? { cursor } : {}) });
    const page = await call(`${rowsPath}?${qs}`);
    yield* page.items;
    cursor = page.next_cursor;
  } while (cursor);
}
```
