nexu-io/open-design · error · Error

openrouter image non-JSON response: ${truncate(text, 200)}

Error message

openrouter image non-JSON response: ${truncate(text, 200)}

What it means

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.

Source

Thrown at apps/daemon/src/media/index.ts:1877

    headers: {
      'authorization': `Bearer ${credentials.apiKey}`,
      'content-type': 'application/json',
      'HTTP-Referer': 'https://opendesign.dev',
      'X-Title': 'Open Design',
    },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(Math.max(OPENAI_IMAGE_HEADERS_TIMEOUT_MS, OPENAI_IMAGE_BODY_TIMEOUT_MS)),
  }));
  const text = await resp.text();
  if (!resp.ok) {
    throw new Error(`openrouter image ${resp.status}: ${truncate(text, 240)}`);
  }

  let data: any;
  try {
    data = JSON.parse(text);
  } catch {
    throw new Error(`openrouter image non-JSON response: ${truncate(text, 200)}`);
  }

  // Extract the first generated image from the response.
  const images: any[] | undefined =
    data?.choices?.[0]?.message?.images;
  if (!images || images.length === 0) {
    throw new Error(
      `openrouter image response contained no images for model ${wireModel}: `
      + truncate(text, 200),
    );
  }

  const dataUrl: string | undefined = images[0]?.image_url?.url;
  if (!dataUrl) {
    throw new Error(
      `openrouter image response missing image_url.url: ${truncate(text, 200)}`,
    );
  }

View on GitHub (pinned to 5be4028344)

Solutions

  1. Inspect the truncated body in the error — HTML tags confirm interception/misroute.
  2. Reset credentials.baseUrl to the default (https://openrouter.ai/api/v1) or remove it.
  3. Bypass any HTTP proxy for openrouter.ai, or whitelist the host.
  4. Retry once for transient interstitials.

Example fix

// before
try { data = JSON.parse(text); }
catch { throw new Error(`openrouter image non-JSON response: ${truncate(text, 200)}`); }

// after — flag HTML interception
try { data = JSON.parse(text); }
catch {
  const html = /<html|<!doctype/i.test(text);
  throw new Error(
    `openrouter image ${html ? 'returned HTML (interception?)' : 'non-JSON response'}: ${truncate(text, 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(text)) {
  throw new Error(`openrouter image returned HTML (proxy/interception or wrong baseUrl): ${truncate(text, 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 {
  data = JSON.parse(text);
} catch {
  if (looksLikeHtmlBody(text)) {
    throw new Error(`openrouter image returned HTML (interception?): ${truncate(text, 200)}`);
  }
  throw new Error(`openrouter image non-JSON response: ${truncate(text, 200)}`);
}

Prevention

When it happens

Trigger: 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.

Common situations: Corporate proxy intercepting openrouter.ai, a typo'd baseUrl hitting a parked domain, or a transient CDN interstitial during a regional outage.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/a160f7738fa26cc5. Report an issue: GitHub.