nexu-io/open-design · error · Error

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

Error message

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

What it means

Thrown by renderOpenAIImage when resp.ok is true but JSON.parse(text) throws. The dispatcher assumes a JSON images/generations response; a non-JSON 200 usually means a man-in-the-middle (corporate proxy, captive portal, service mesh) returned an HTML page with a 200 status, or the baseUrl was pointed at an endpoint that does not speak the OpenAI schema.

Source

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

  const resp = await fetch(url, withMediaRequestInit(ctx, {
    method: 'POST',
    headers,
    body: JSON.stringify(body),
    dispatcher: ctx.requestInit.dispatcher
      ?? openAIImageDispatcher as unknown as NonNullable<RequestInit['dispatcher']>,
    signal: AbortSignal.timeout(Math.max(OPENAI_IMAGE_HEADERS_TIMEOUT_MS, OPENAI_IMAGE_BODY_TIMEOUT_MS)),
  }));
  const text = await resp.text();
  if (!resp.ok) {
    const tag = azure ? 'azure-openai' : 'openai';
    throw new Error(`${tag} ${resp.status}: ${truncate(text, 240)}`);
  }
  let data: any;
  try {
    data = JSON.parse(text);
  } catch {
    throw new Error(`openai non-JSON response: ${truncate(text, 200)}`);
  }
  const entry = data && Array.isArray(data.data) ? data.data[0] : null;
  if (!entry) throw new Error('openai response had no data[0]');
  let bytes;
  if (entry.b64_json) {
    bytes = Buffer.from(entry.b64_json, 'base64');
  } else if (entry.url) {
    const imgResp = await fetch(entry.url, withMediaRequestInit(ctx));
    if (!imgResp.ok) throw new Error(`openai image fetch ${imgResp.status}`);
    const arr = await imgResp.arrayBuffer();
    bytes = Buffer.from(arr);
  } else {
    throw new Error('openai response had neither b64_json nor url');
  }

  const tag = azure ? 'azure-openai' : 'openai';
  return {
    bytes,

View on GitHub (pinned to 5be4028344)

Solutions

  1. Inspect the truncated body in the message to identify what returned the non-JSON response (proxy page, gateway error, etc.).
  2. Verify credentials.baseUrl resolves to a real OpenAI/Azure endpoint and not a UI or docs host.
  3. Bypass or reconfigure the intercepting proxy for the OpenAI host, or point baseUrl at the proxy's true API path.
  4. If running a custom OpenAI-compatible server, ensure it returns application/json with the {data:[...]} schema on success.

Example fix

// before
OD_OPENAI_BASE_URL=https://gateway.internal/
// after (point at the API path the gateway actually serves)
OD_OPENAI_BASE_URL=https://gateway.internal/v1
Defensive patterns

Strategy: try-catch

Validate before calling

function isOpenAIJsonEndpoint(baseUrl: string): boolean {
  // crude guard: must look like an API host, not a UI/docs host
  return /^https:\/\/[\w.-]+\.openai\.com(?:\/.*)?$/.test(baseUrl)
    || /\/v1$/.test(baseUrl)
    || /openai\.azure\.com/.test(baseUrl);
}

Try / catch

try {
  return await renderOpenAIImage(ctx, credentials);
} catch (err) {
  const m = err instanceof Error ? err.message : '';
  if (m.startsWith('openai non-JSON response:')) {
    // surface the body snippet to the user — almost always a proxy/gateway issue
    throw new ConfigError(`OpenAI returned HTML/text, not JSON. Check baseUrl and proxies. Body: ${m}`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Custom baseUrl pointed at a browser-facing gateway that returns HTML; corporate TLS-intercepting proxy injecting a block page; misconfigured reverse proxy returning a health-check page; baseUrl pointing at /v1 instead of the host root.

Common situations: Self-hosted gateway in front of OpenAI that 200s with HTML on unknown paths; Azure app-gateway rewrite rule stripping the body; local mock server returning text/plain.

Related errors


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