{"record":{"id":"b06b9fd659f8fce5","repo":"nexu-io/open-design","slug":"openrouter-poll-non-json-truncate-polltext-200","errorCode":null,"errorMessage":"openrouter poll non-JSON: ${truncate(pollText, 200)}","messagePattern":"openrouter poll non-JSON: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":2092,"sourceCode":"    }\n    const pollResp = await fetch(pollingUrl, withMediaRequestInit(ctx, {\n      headers: {\n        'authorization': `Bearer ${credentials.apiKey}`,\n        'HTTP-Referer': 'https://opendesign.dev',\n        'X-Title': 'Open Design',\n      },\n    }));\n    const pollText = await pollResp.text();\n    if (!pollResp.ok) {\n      throw new Error(\n        `openrouter poll ${pollResp.status}: ${truncate(pollText, 240)}`,\n      );\n    }\n    let pollData: any;\n    try {\n      pollData = JSON.parse(pollText);\n    } catch {\n      throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`);\n    }\n\n    lastStatus = pollData?.status || '';\n    if (typeof onProgress === 'function') {\n      const elapsedSec = Math.round((Date.now() - startedAt) / 1000);\n      onProgress(\n        `openrouter job ${jobId} status=${lastStatus || 'pending'} (elapsed ${elapsedSec}s)`,\n      );\n    }\n\n    if (lastStatus === 'completed') {\n      videoUrls = pollData?.unsigned_urls || null;\n      break;\n    }\n    if (\n      lastStatus === 'failed'\n      || lastStatus === 'expired'\n      || lastStatus === 'cancelled'","sourceCodeStart":2074,"sourceCodeEnd":2110,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L2074-L2110","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\ntry { pollData = JSON.parse(pollText); }\ncatch { throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`); }\n\n// after — flag HTML and continue polling instead of aborting\ntry { pollData = JSON.parse(pollText); }\ncatch {\n  const html = /<html|<!doctype/i.test(pollText);\n  if (html) { /* keep polling next iteration */ continue; }\n  throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`);\n}","handlingStrategy":"validation","validationCode":"function looksLikeHtmlBody(text: string): boolean {\n  return /<html|<!doctype|<title>/i.test(text.slice(0, 200));\n}\n\n// Inside the poll loop: skip this iteration rather than aborting on transient HTML\nif (looksLikeHtmlBody(pollText)) {\n  // transient interstitial — keep polling, do not throw\n  continue;\n}","typeGuard":"function isJsonContentType(resp: Response): boolean {\n  const ct = resp.headers.get('content-type') || '';\n  return ct.includes('application/json') || ct.includes('+json');\n}","tryCatchPattern":"try {\n  pollData = JSON.parse(pollText);\n} catch {\n  if (looksLikeHtmlBody(pollText)) {\n    // skip this poll, keep the loop alive\n    continue;\n  }\n  throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`);\n}","preventionTips":["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."],"tags":["openrouter","video-generation","async-polling","response-shape","network","provider-api"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}