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
- Tune OD_OPENROUTER_VIDEO_POLL_INTERVAL_MS upward to avoid 429s on the polling endpoint.
- If polling_url expiry is the cause, lower OD_OPENROUTER_VIDEO_MAX_POLL_MS expectations or pick a faster video model.
- On 5xx, the existing loop does not retry — wrap the poll in a small backoff before giving up.
- 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
- Tune OD_OPENROUTER_VIDEO_POLL_INTERVAL_MS upward (>=5s) to avoid 429 on the polling endpoint.
- Retry transient 5xx/429 inside the loop before aborting the whole job.
- If polling URLs expire mid-job, choose a faster video model.
- On 404 mid-poll, restart the render rather than continuing.
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
- openrouter poll non-JSON: ${truncate(pollText, 200)}
- openrouter video submit ${submitResp.status}: ${truncate(sub
- openrouter video non-JSON: ${truncate(submitText, 200)}
- openrouter video submit returned no job id or polling_url: $
- openrouter job ${lastStatus}: ${reason}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/ebd65dc2ebb4cd99.
Report an issue: GitHub.