nexu-io/open-design · error · Error

openrouter poll non-JSON: ${truncate(pollText, 200)}

Error message

openrouter poll non-JSON: ${truncate(pollText, 200)}

What it means

Thrown inside the polling loop when GET pollingUrl returned 2xx but JSON.parse failed on pollText. OpenRouter status polls return JSON (status, unsigned_urls, error); a non-JSON 2xx means an interceptor rewrote the response or the polling host returned HTML.

Source

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

    }
    const pollResp = await fetch(pollingUrl, withMediaRequestInit(ctx, {
      headers: {
        'authorization': `Bearer ${credentials.apiKey}`,
        'HTTP-Referer': 'https://opendesign.dev',
        'X-Title': 'Open Design',
      },
    }));
    const pollText = await pollResp.text();
    if (!pollResp.ok) {
      throw new Error(
        `openrouter poll ${pollResp.status}: ${truncate(pollText, 240)}`,
      );
    }
    let pollData: any;
    try {
      pollData = JSON.parse(pollText);
    } catch {
      throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`);
    }

    lastStatus = pollData?.status || '';
    if (typeof onProgress === 'function') {
      const elapsedSec = Math.round((Date.now() - startedAt) / 1000);
      onProgress(
        `openrouter job ${jobId} status=${lastStatus || 'pending'} (elapsed ${elapsedSec}s)`,
      );
    }

    if (lastStatus === 'completed') {
      videoUrls = pollData?.unsigned_urls || null;
      break;
    }
    if (
      lastStatus === 'failed'
      || lastStatus === 'expired'
      || lastStatus === 'cancelled'

View on GitHub (pinned to 5be4028344)

Solutions

  1. Inspect pollText in the error — HTML tags confirm interception.
  2. Whitelist the polling host through any HTTP proxy.
  3. Retry the render; transient interstitials resolve.
  4. If persistent, report to OpenRouter — polling hosts should always return JSON.

Example fix

// before
try { pollData = JSON.parse(pollText); }
catch { throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`); }

// after — flag HTML and continue polling instead of aborting
try { pollData = JSON.parse(pollText); }
catch {
  const html = /<html|<!doctype/i.test(pollText);
  if (html) { /* keep polling next iteration */ continue; }
  throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`);
}
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeHtmlBody(text: string): boolean {
  return /<html|<!doctype|<title>/i.test(text.slice(0, 200));
}

// Inside the poll loop: skip this iteration rather than aborting on transient HTML
if (looksLikeHtmlBody(pollText)) {
  // transient interstitial — keep polling, do not throw
  continue;
}

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 {
  pollData = JSON.parse(pollText);
} catch {
  if (looksLikeHtmlBody(pollText)) {
    // skip this poll, keep the loop alive
    continue;
  }
  throw new Error(`openrouter poll non-JSON: ${truncate(pollText, 200)}`);
}

Prevention

When it happens

Trigger: Proxy/CDN returns an HTML page with 200 on the polling host, polling_url points at a host that serves interstitials, or a captive portal intercepts the poll. The guard prevents the status/urls extraction from running on garbage.

Common situations: Corporate proxy blocking the polling host with an HTML block page, transient interstitial during a regional outage, or a polling_url pointing at a deprecated host.

Related errors


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