nexu-io/open-design · error · Error

grok video non-JSON: ${truncate(submitText, 200)}

Error message

grok video non-JSON: ${truncate(submitText, 200)}

What it means

Thrown when xAI's POST /videos/generations returned 2xx but the body failed JSON.parse. The server returned a non-JSON payload (HTML, plain text, empty) with a success status. The message includes the first 200 chars (truncated) so the operator can identify the unexpected content shape. Distinct from [472] which fires on non-2xx status.

Source

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

  }

  const submitResp = await fetch(`${baseUrl}/videos/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(`grok video submit ${submitResp.status}: ${truncate(submitText, 240)}`);
  }
  let submitData: any;
  try {
    submitData = JSON.parse(submitText);
  } catch {
    throw new Error(`grok video non-JSON: ${truncate(submitText, 200)}`);
  }

  // Two paths: (a) the API returned the finished video synchronously
  // (cached/short jobs), in which case we skip polling; (b) we got an
  // {id, status:'pending'} stub and need to poll GET /videos/{id}
  // until status flips to done/failed/expired.
  let videoUrl = submitData?.video?.url || null;
  let lastStatus = submitData?.status || '';
  const requestId = submitData?.id || submitData?.request_id || null;

  if (!videoUrl && requestId) {
    const startedAt = Date.now();
    const configuredMaxMs = Number(process.env.OD_GROK_VIDEO_MAX_POLL_MS);
    const maxMs =
      Number.isFinite(configuredMaxMs) && configuredMaxMs >= 60_000
        ? configuredMaxMs
        : 8 * 60 * 1000;
    if (typeof onProgress === 'function') {

View on GitHub (pinned to 5be4028344)

Solutions

  1. Inspect the truncated body in the error — HTML/doctype means gateway or maintenance page; retry shortly.
  2. If you set a custom baseUrl, confirm it points at 'https://api.x.ai/v1' (or your xAI deployment's JSON API root), not an HTML surface.
  3. Retry with exponential backoff — interstitials are usually transient.
  4. If persistent, capture the full response and report to xAI support.

Example fix

// before — wrong baseUrl returns HTML
credentials.baseUrl = 'https://x.ai'
// → 200 OK with HTML homepage → [473]

// after
credentials.baseUrl = 'https://api.x.ai/v1'
Defensive patterns

Strategy: try-catch

Validate before calling

// Reject a misconfigured baseUrl before it ever produces HTML responses.
function assertGrokBaseUrl(baseUrl?: string): void {
  if (!baseUrl) return;
  try {
    const u = new URL(baseUrl);
    if (u.hostname !== 'api.x.ai' && !u.hostname.endsWith('.api.x.ai')) {
      throw new Error(`xAI baseUrl host looks wrong (${u.hostname}); expected api.x.ai`);
    }
  } 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 {
  const looksLikeHtml = /^\s*<(?:!doctype|html|body)/i.test(submitText);
  throw new Error(
    `grok video non-JSON (${looksLikeHtml ? 'html/gateway' : 'unknown'}): ${truncate(submitText, 200)}`,
  );
}

Prevention

When it happens

Trigger: submitResp.ok is true but JSON.parse(submitText) throws — xAI (or an intermediary) returned 200 with an HTML page, a Cloudflare challenge, an empty body, or plain text. Often a transient gateway behavior.

Common situations: Cloudflare interstitial served with 200; xAI gateway returning a cached HTML error page; reverse proxy in front of api.x.ai rewriting responses; baseUrl in provider config pointing at a non-API host.

Related errors


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