nexu-io/open-design · error · Error
openrouter poll non-JSON: ${truncate(pollText, 200)}
Error message
openrouter poll non-JSON: ${truncate(pollText, 200)} What it means
Thrown inside the polling loop when GET pollingUrl returned 2xx but JSON.parse failed on pollText. OpenRouter status polls return JSON (status, unsigned_urls, error); a non-JSON 2xx means an interceptor rewrote the response or the polling host returned HTML.
Source
Thrown at apps/daemon/src/media/index.ts:2092
}
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)`,
);
}
if (lastStatus === 'completed') {
videoUrls = pollData?.unsigned_urls || null;
break;
}
if (
lastStatus === 'failed'
|| lastStatus === 'expired'
|| lastStatus === 'cancelled'View on GitHub (pinned to 5be4028344)
Solutions
- Inspect pollText in the error — HTML tags confirm interception.
- Whitelist the polling host through any HTTP proxy.
- Retry the render; transient interstitials resolve.
- If persistent, report to OpenRouter — polling hosts should always return JSON.
Example fix
// before
try { pollData = JSON.parse(pollText); }
catch { throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`); }
// after — flag HTML and continue polling instead of aborting
try { pollData = JSON.parse(pollText); }
catch {
const html = /<html|<!doctype/i.test(pollText);
if (html) { /* keep polling next iteration */ continue; }
throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`);
} Defensive patterns
Strategy: validation
Validate before calling
function looksLikeHtmlBody(text: string): boolean {
return /<html|<!doctype|<title>/i.test(text.slice(0, 200));
}
// Inside the poll loop: skip this iteration rather than aborting on transient HTML
if (looksLikeHtmlBody(pollText)) {
// transient interstitial — keep polling, do not throw
continue;
} Type guard
function isJsonContentType(resp: Response): boolean {
const ct = resp.headers.get('content-type') || '';
return ct.includes('application/json') || ct.includes('+json');
} Try / catch
try {
pollData = JSON.parse(pollText);
} catch {
if (looksLikeHtmlBody(pollText)) {
// skip this poll, keep the loop alive
continue;
}
throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`);
} Prevention
- Check content-type and body prefix before parsing each poll.
- On transient HTML interstitials, skip the iteration and keep polling instead of aborting the job.
- Whitelist the polling host through corporate proxies.
- Log the first 200 chars of non-JSON poll bodies for diagnosis.
When it happens
Trigger: Proxy/CDN returns an HTML page with 200 on the polling host, polling_url points at a host that serves interstitials, or a captive portal intercepts the poll. The guard prevents the status/urls extraction from running on garbage.
Common situations: Corporate proxy blocking the polling host with an HTML block page, transient interstitial during a regional outage, or a polling_url pointing at a deprecated host.
Related errors
- openrouter video non-JSON: ${truncate(submitText, 200)}
- openrouter video submit returned no job id or polling_url: $
- openrouter poll ${pollResp.status}: ${truncate(pollText, 240
- openrouter image non-JSON response: ${truncate(text, 200)}
- openrouter job ${lastStatus}: ${reason}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/b06b9fd659f8fce5.
Report an issue: GitHub.