nexu-io/open-design · error · Error
openrouter video non-JSON: ${truncate(submitText, 200)}
Error message
openrouter video non-JSON: ${truncate(submitText, 200)} What it means
Thrown when the video submit endpoint returned HTTP 2xx but JSON.parse failed on the body. OpenRouter's async video submit is contractually JSON (carrying id and polling_url); a non-JSON 2xx means an interceptor rewrote the response or baseUrl is misrouted to a host returning HTML.
Source
Thrown at apps/daemon/src/media/index.ts:2037
'content-type': 'application/json',
// OpenRouter attribution headers per
// https://openrouter.ai/docs/app-attribution
'HTTP-Referer': 'https://opendesign.dev',
'X-Title': 'Open Design',
},
body: JSON.stringify(body),
}));
const submitText = await submitResp.text();
if (!submitResp.ok) {
throw new Error(
`openrouter video submit ${submitResp.status}: ${truncate(submitText, 240)}`,
);
}
let submitData: any;
try {
submitData = JSON.parse(submitText);
} catch {
throw new Error(`openrouter video non-JSON: ${truncate(submitText, 200)}`);
}
const jobId = submitData?.id;
const pollingUrl = submitData?.polling_url;
if (!jobId || !pollingUrl) {
throw new Error(
`openrouter video submit returned no job id or polling_url: ${truncate(submitText, 200)}`,
);
}
// ── Step 2: Poll until completion ──────────────────────────────────
const startedAt = Date.now();
const configuredMaxMs = Number(process.env.OD_OPENROUTER_VIDEO_MAX_POLL_MS);
const maxMs =
Number.isFinite(configuredMaxMs) && configuredMaxMs >= 60_000
? configuredMaxMs
: 30 * 60 * 1000; // 30 minutes default
const configuredPollIntervalMs = Number(process.env.OD_OPENROUTER_VIDEO_POLL_INTERVAL_MS);View on GitHub (pinned to 5be4028344)
Solutions
- Inspect the truncated body in the error — HTML/doctype tags indicate interception/misroute.
- Reset credentials.baseUrl to the default or remove it.
- Whitelist openrouter.ai through any HTTP proxy.
- Retry once for transient interstitials.
Example fix
// before
try { submitData = JSON.parse(submitText); }
catch { throw new Error(`openrouter video non-JSON: ${truncate(submitText, 200)}`); }
// after — distinguish HTML interception
try { submitData = JSON.parse(submitText); }
catch {
const html = /<html|<!doctype/i.test(submitText);
throw new Error(
`openrouter video ${html ? 'returned HTML (interception?)' : 'non-JSON'}: ${truncate(submitText, 200)}`,
);
} Defensive patterns
Strategy: validation
Validate before calling
function looksLikeHtmlBody(text: string): boolean {
return /<html|<!doctype|<title>/i.test(text.slice(0, 200));
}
if (looksLikeHtmlBody(submitText)) {
throw new Error(`openrouter video submit returned HTML (proxy/interception or wrong baseUrl): ${truncate(submitText, 200)}`);
} 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 {
submitData = JSON.parse(submitText);
} catch {
if (looksLikeHtmlBody(submitText)) {
throw new Error(`openrouter video returned HTML (interception?): ${truncate(submitText, 200)}`);
}
throw new Error(`openrouter video non-JSON: ${truncate(submitText, 200)}`);
} Prevention
- Validate content-type and body prefix before parsing.
- Reset baseUrl to the default OpenRouter endpoint when interception is suspected.
- Whitelist openrouter.ai through corporate proxies.
- Log the first 200 chars of non-JSON bodies for diagnosis.
When it happens
Trigger: Proxy/CDN served an HTML page with 200, baseUrl points at a wrong host, or a captive portal intercepted the request. The guard stops the subsequent id/polling_url extraction from running on non-JSON input.
Common situations: Corporate proxy blocking openrouter.ai with an HTML block page, baseUrl typo hitting a parked/marketing domain, or a transient CDN interstitial.
Related errors
- openrouter poll non-JSON: ${truncate(pollText, 200)}
- openrouter image non-JSON response: ${truncate(text, 200)}
- openrouter video submit returned no job id or polling_url: $
- openrouter poll ${pollResp.status}: ${truncate(pollText, 240
- nano-banana image non-JSON: ${truncate(text, 200)}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/cdfedee0e1a06c9d.
Report an issue: GitHub.