nexu-io/open-design · error · Error
leonardo.ai non-JSON: ${truncate(submitText, 200)}
Error message
leonardo.ai non-JSON: ${truncate(submitText, 200)} What it means
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.
Source
Thrown at apps/daemon/src/media/index.ts:2241
const submitResp = await fetch(`${baseUrl}/generations`, withMediaRequestInit(ctx, {
method: 'POST',
headers: {
'authorization': `Bearer ${credentials.apiKey}`,
'content-type': 'application/json',
},
body: JSON.stringify(body),
}));
const submitText = await submitResp.text();
if (!submitResp.ok) {
throw new Error(`leonardo.ai submit ${submitResp.status}: ${truncate(submitText, 240)}`);
}
let submitData: any;
try {
submitData = JSON.parse(submitText);
} catch {
throw new Error(`leonardo.ai non-JSON: ${truncate(submitText, 200)}`);
}
const generationId = submitData?.sdGenerationJob?.generationId;
if (!generationId) {
throw new Error('leonardo.ai response missing generationId');
}
// Poll for completion
const maxPollMs = 120000; // 2 minutes
const pollIntervalMs = 2000; // 2 seconds
const startedAt = Date.now();
let imageUrl: string | null = null;
while (Date.now() - startedAt < maxPollMs) {
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
const pollResp = await fetch(`${baseUrl}/generations/${generationId}`, withMediaRequestInit(ctx, {
headers: {View on GitHub (pinned to 5be4028344)
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.
Example fix
// before — wrong baseUrl points at HTML site credentials.baseUrl = 'https://leonardo.ai' // → 200 OK with HTML → [465] // after credentials.baseUrl = 'https://cloud.leonardo.ai/api/rest/v1'
Defensive patterns
Strategy: try-catch
Validate before calling
// Reject a misconfigured baseUrl before it ever produces HTML responses.
function assertLeonardoBaseUrl(baseUrl?: string): void {
if (!baseUrl) return; // default is fine
try {
const u = new URL(baseUrl);
if (!u.pathname.includes('/api/rest')) {
throw new Error(`Leonardo baseUrl looks like an HTML surface (${baseUrl}); expected .../api/rest/v1`);
}
} catch (e) { throw e; }
} Try / catch
// Distinguish parse failure from real HTTP failure so the operator gets the right hint.
let submitData: any;
try {
submitData = JSON.parse(submitText);
} catch {
// If it's HTML, this is almost certainly a gateway/maintenance page — retryable.
const looksLikeHtml = /^\s*<(?:!doctype|html|body)/i.test(submitText);
throw new Error(
`leonardo.ai non-JSON (${looksLikeHtml ? 'html/gateway' : 'unknown'}): ${truncate(submitText, 200)}`,
);
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- grok video non-JSON: ${truncate(submitText, 200)}
- no Leonardo.ai API key — configure it in Settings or set LEO
- unsupported leonardo.ai model: ${ctx.model}
- leonardo.ai generation timed out after 2 minutes
- grok poll non-JSON: ${truncate(pollText, 200)}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/e82d53efc01fdb8e.
Report an issue: GitHub.