{"record":{"id":"a160f7738fa26cc5","repo":"nexu-io/open-design","slug":"openrouter-image-non-json-response-truncate-tex","errorCode":null,"errorMessage":"openrouter image non-JSON response: ${truncate(text, 200)}","messagePattern":"openrouter image non-JSON response: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":1877,"sourceCode":"    headers: {\n      'authorization': `Bearer ${credentials.apiKey}`,\n      'content-type': 'application/json',\n      'HTTP-Referer': 'https://opendesign.dev',\n      'X-Title': 'Open Design',\n    },\n    body: JSON.stringify(body),\n    signal: AbortSignal.timeout(Math.max(OPENAI_IMAGE_HEADERS_TIMEOUT_MS, OPENAI_IMAGE_BODY_TIMEOUT_MS)),\n  }));\n  const text = await resp.text();\n  if (!resp.ok) {\n    throw new Error(`openrouter image ${resp.status}: ${truncate(text, 240)}`);\n  }\n\n  let data: any;\n  try {\n    data = JSON.parse(text);\n  } catch {\n    throw new Error(`openrouter image non-JSON response: ${truncate(text, 200)}`);\n  }\n\n  // Extract the first generated image from the response.\n  const images: any[] | undefined =\n    data?.choices?.[0]?.message?.images;\n  if (!images || images.length === 0) {\n    throw new Error(\n      `openrouter image response contained no images for model ${wireModel}: `\n      + truncate(text, 200),\n    );\n  }\n\n  const dataUrl: string | undefined = images[0]?.image_url?.url;\n  if (!dataUrl) {\n    throw new Error(\n      `openrouter image response missing image_url.url: ${truncate(text, 200)}`,\n    );\n  }","sourceCodeStart":1859,"sourceCodeEnd":1895,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L1859-L1895","documentation":"Thrown by renderOpenRouterImage when HTTP returned 2xx but JSON.parse fails on the body. OpenRouter normally returns JSON for chat completions; a non-JSON 2xx typically means an interceptor (proxy, captive portal) rewrote the response, or a misconfigured baseUrl pointed at a host returning HTML.","triggerScenarios":"Reverse proxy returns an HTML ' upstream' page with 200, baseUrl points at a non-OpenRouter host, or a CDN served a cached error page. The guard prevents the subsequent choices[0].message.images extraction from running on garbage.","commonSituations":"Corporate proxy intercepting openrouter.ai, a typo'd baseUrl hitting a parked domain, or a transient CDN interstitial during a regional outage.","solutions":["Inspect the truncated body in the error — HTML tags confirm interception/misroute.","Reset credentials.baseUrl to the default (https://openrouter.ai/api/v1) or remove it.","Bypass any HTTP proxy for openrouter.ai, or whitelist the host.","Retry once for transient interstitials."],"exampleFix":"// before\ntry { data = JSON.parse(text); }\ncatch { throw new Error(`openrouter image non-JSON response: ${truncate(text, 200)}`); }\n\n// after — flag HTML interception\ntry { data = JSON.parse(text); }\ncatch {\n  const html = /<html|<!doctype/i.test(text);\n  throw new Error(\n    `openrouter image ${html ? 'returned HTML (interception?)' : 'non-JSON response'}: ${truncate(text, 200)}`,\n  );\n}","handlingStrategy":"validation","validationCode":"function looksLikeHtmlBody(text: string): boolean {\n  return /<html|<!doctype|<title>/i.test(text.slice(0, 200));\n}\n\nif (looksLikeHtmlBody(text)) {\n  throw new Error(`openrouter image returned HTML (proxy/interception or wrong baseUrl): ${truncate(text, 200)}`);\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  data = JSON.parse(text);\n} catch {\n  if (looksLikeHtmlBody(text)) {\n    throw new Error(`openrouter image returned HTML (interception?): ${truncate(text, 200)}`);\n  }\n  throw new Error(`openrouter image non-JSON response: ${truncate(text, 200)}`);\n}","preventionTips":["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."],"tags":["openrouter","image-generation","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"}