nexu-io/open-design · error · Error

openrouter poll ${pollResp.status}: ${truncate(pollText, 240

Error message

openrouter poll ${pollResp.status}: ${truncate(pollText, 240)}

What it means

Thrown inside the video polling loop when GET pollingUrl returns non-2xx. Each iteration fetches pollText and, on failure, embeds pollResp.status plus the first 240 chars. A failed poll aborts the whole job rather than continuing — the daemon cannot determine job state from a non-2xx.

Source

Thrown at apps/daemon/src/media/index.ts:2084

    onProgress(
      `openrouter ${mode} job ${jobId} (${wireModel}) accepted; polling status…`,
    );
  }

  while (Date.now() - startedAt < maxMs) {
    if (pollIntervalMs > 0) {
      await sleep(pollIntervalMs);
    }
    const pollResp = await fetch(pollingUrl, withMediaRequestInit(ctx, {
      headers: {
        'authorization': `Bearer ${credentials.apiKey}`,
        'HTTP-Referer': 'https://opendesign.dev',
        'X-Title': 'Open Design',
      },
    }));
    const pollText = await pollResp.text();
    if (!pollResp.ok) {
      throw new Error(
        `openrouter poll ${pollResp.status}: ${truncate(pollText, 240)}`,
      );
    }
    let pollData: any;
    try {
      pollData = JSON.parse(pollText);
    } catch {
      throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`);
    }

    lastStatus = pollData?.status || '';
    if (typeof onProgress === 'function') {
      const elapsedSec = Math.round((Date.now() - startedAt) / 1000);
      onProgress(
        `openrouter job ${jobId} status=${lastStatus || 'pending'} (elapsed ${elapsedSec}s)`,
      );
    }

View on GitHub (pinned to 5be4028344)

Solutions

  1. Tune OD_OPENROUTER_VIDEO_POLL_INTERVAL_MS upward to avoid 429s on the polling endpoint.
  2. If polling_url expiry is the cause, lower OD_OPENROUTER_VIDEO_MAX_POLL_MS expectations or pick a faster video model.
  3. On 5xx, the existing loop does not retry — wrap the poll in a small backoff before giving up.
  4. On 401/403 re-check the key; on 404 the job is gone — restart the render from scratch.

Example fix

// before
if (!pollResp.ok) {
  throw new Error(`openrouter poll ${pollResp.status}: ${truncate(pollText, 240)}`);
}

// after — tolerate transient 5xx/429 with bounded retries inside the loop
let pollResp = await fetch(pollingUrl, /* ... */);
let pollText = await pollResp.text();
for (let attempt = 0; attempt < 2 && !pollResp.ok && (pollResp.status === 429 || pollResp.status >= 500); attempt++) {
  await sleep(2000 * (attempt + 1));
  pollResp = await fetch(pollingUrl, /* ... */);
  pollText = await pollResp.text();
}
if (!pollResp.ok) {
  throw new Error(`openrouter poll ${pollResp.status}: ${truncate(pollText, 240)}`);
}
Defensive patterns

Strategy: retry

Validate before calling

// Bound the polling interval to avoid 429s
function safePollIntervalMs(env = process.env): number {
  const v = Number(env.OD_OPENROUTER_VIDEO_POLL_INTERVAL_MS);
  return Number.isFinite(v) && v >= 5000 ? v : 10_000; // never poll faster than every 5s
}

function isRetryablePollStatus(status: number): boolean {
  return status === 429 || status === 408 || status >= 500;
}

Type guard

function isPollUrlStillValid(pollingUrl: string): boolean {
  // crude check — must still be https and non-empty
  return /^https:\/\//.test(pollingUrl);
}

Try / catch

let pollData: any;
for (let attempt = 0; attempt < 3; attempt++) {
  const pollResp = await fetch(pollingUrl, /* auth headers */);
  if (pollResp.ok) { pollData = JSON.parse(await pollResp.text()); break; }
  if (isRetryablePollStatus(pollResp.status) && attempt < 2) {
    await sleep(2000 * (attempt + 1));
    continue;
  }
  throw new Error(`openrouter poll ${pollResp.status}: ${truncate(await pollResp.text(), 240)}`);
}

Prevention

When it happens

Trigger: polling_url expired or was revoked mid-job, OpenRouter rate-limits the polling endpoint (429), the job was garbage-collected on the provider side (404), auth issues (401/403), or upstream 5xx during polling.

Common situations: Long-running jobs whose polling_url TTL lapses before completion, aggressive poll intervals triggering 429, provider-side job expiry, or transient 5xx during a provider incident.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/ebd65dc2ebb4cd99. Report an issue: GitHub.