{"record":{"id":"e82d53efc01fdb8e","repo":"nexu-io/open-design","slug":"leonardo-ai-non-json-truncate-submittext-200","errorCode":null,"errorMessage":"leonardo.ai non-JSON: ${truncate(submitText, 200)}","messagePattern":"leonardo\\.ai non-JSON: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":2241,"sourceCode":"  const submitResp = await fetch(`${baseUrl}/generations`, withMediaRequestInit(ctx, {\n    method: 'POST',\n    headers: {\n      'authorization': `Bearer ${credentials.apiKey}`,\n      'content-type': 'application/json',\n    },\n    body: JSON.stringify(body),\n  }));\n  \n  const submitText = await submitResp.text();\n  if (!submitResp.ok) {\n    throw new Error(`leonardo.ai submit ${submitResp.status}: ${truncate(submitText, 240)}`);\n  }\n  \n  let submitData: any;\n  try {\n    submitData = JSON.parse(submitText);\n  } catch {\n    throw new Error(`leonardo.ai non-JSON: ${truncate(submitText, 200)}`);\n  }\n  \n  const generationId = submitData?.sdGenerationJob?.generationId;\n  if (!generationId) {\n    throw new Error('leonardo.ai response missing generationId');\n  }\n  \n  // Poll for completion\n  const maxPollMs = 120000; // 2 minutes\n  const pollIntervalMs = 2000; // 2 seconds\n  const startedAt = Date.now();\n  let imageUrl: string | null = null;\n  \n  while (Date.now() - startedAt < maxPollMs) {\n    await new Promise(resolve => setTimeout(resolve, pollIntervalMs));\n    \n    const pollResp = await fetch(`${baseUrl}/generations/${generationId}`, withMediaRequestInit(ctx, {\n      headers: {","sourceCodeStart":2223,"sourceCodeEnd":2259,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L2223-L2259","documentation":"Thrown when Leonardo.ai's POST /generations returned 2xx but the body failed JSON.parse. This means the server returned a 2xx status with a non-JSON payload (HTML error page, plain text, empty body). The message includes the first 200 chars (truncated) of whatever came back so the operator can identify the content shape.","triggerScenarios":"submitResp.ok is true but JSON.parse(submitText) throws — Leonardo returns 200 with an HTML maintenance page, a Cloudflare interstitial, an empty string, or a plain-text error. Distinct from [464] which fires on non-2xx status.","commonSituations":"Cloudflare/WAF challenge page served with 200; Leonardo API gateway returning a cached HTML error; proxy or reverse-proxy in front of the API rewriting the response; baseUrl misconfigured to point at a non-API host that serves HTML.","solutions":["Inspect the truncated body in the error — HTML/doctype indicates a gateway or maintenance page; retry shortly.","If you set a custom baseUrl in provider config, confirm it points at the JSON API root (…/api/rest/v1) and not at the marketing site or a console URL.","If a Cloudflare challenge is persistent, route the daemon through a network egress that is not flagged, or contact Leonardo support.","Retry with exponential backoff — interstitials are usually transient."],"exampleFix":"// before — wrong baseUrl points at HTML site\ncredentials.baseUrl = 'https://leonardo.ai'\n// → 200 OK with HTML → [465]\n\n// after\ncredentials.baseUrl = 'https://cloud.leonardo.ai/api/rest/v1'","handlingStrategy":"try-catch","validationCode":"// Reject a misconfigured baseUrl before it ever produces HTML responses.\nfunction assertLeonardoBaseUrl(baseUrl?: string): void {\n  if (!baseUrl) return; // default is fine\n  try {\n    const u = new URL(baseUrl);\n    if (!u.pathname.includes('/api/rest')) {\n      throw new Error(`Leonardo baseUrl looks like an HTML surface (${baseUrl}); expected .../api/rest/v1`);\n    }\n  } catch (e) { throw e; }\n}","typeGuard":null,"tryCatchPattern":"// Distinguish parse failure from real HTTP failure so the operator gets the right hint.\nlet submitData: any;\ntry {\n  submitData = JSON.parse(submitText);\n} catch {\n  // If it's HTML, this is almost certainly a gateway/maintenance page — retryable.\n  const looksLikeHtml = /^\\s*<(?:!doctype|html|body)/i.test(submitText);\n  throw new Error(\n    `leonardo.ai non-JSON (${looksLikeHtml ? 'html/gateway' : 'unknown'}): ${truncate(submitText, 200)}`,\n  );\n}","preventionTips":["Validate baseUrl shape (must include /api/rest) when the user saves provider config.","Use JSON-only accept headers (`Accept: application/json`) to discourage gateways from serving HTML.","In staging, run a smoke render after any Leonardo config change to catch HTML-gateway regressions early."],"tags":["leonardo","json-parse","gateway","configuration"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}