tinyhumansai/openhuman · error · Error

Failed to parse service CLI output as JSON: ${err instanceof

Error message

Failed to parse service CLI output as JSON: ${err instanceof Error ? err.message : String(err)}

What it means

parseServiceCliOutput() takes the raw stdout of a service CLI invocation and JSON.parses it, expecting a CommandResponse envelope. This variant is thrown when JSON.parse itself fails — the bytes on stdout are not JSON at all: human-readable error text, panic messages, log lines, shell diagnostics, or an empty string.

Source

Thrown at app/src/utils/tauriCommands/common.ts:92

  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return false;
  }
  const candidate = value as { result?: unknown; logs?: unknown };
  if (!('result' in candidate) || !('logs' in candidate)) {
    return false;
  }
  if (!Array.isArray(candidate.logs)) {
    return false;
  }
  return candidate.logs.every(entry => typeof entry === 'string');
}

export function parseServiceCliOutput<T>(raw: string): CommandResponse<T> {
  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch (err) {
    throw new Error(
      `Failed to parse service CLI output as JSON: ${err instanceof Error ? err.message : String(err)}`
    );
  }
  if (!isCommandResponse<T>(parsed)) {
    throw new Error(
      'Failed to parse service CLI output as JSON: parsed value does not match CommandResponse shape'
    );
  }
  return parsed;
}

/**
 * Typed marker for the CEF "IPC bridge not wired" failure mode. The vendored
 * `app/src-tauri/vendor/tauri-cef/crates/tauri/scripts/ipc-protocol.js` falls
 * back to `window.ipc.postMessage(...)` whenever the custom-protocol fetch
 * rejects (network blip, navigation interrupt, mid-session re-entry). On CEF
 * `window.ipc` is never wired — `app/src-tauri/src/cef_impl.rs` drops the
 * `ipc_handler` registration — so the fallback throws

View on GitHub (pinned to a221052e0d)

Solutions

  1. Capture and log the raw string when parsing fails so you can see exactly what the CLI printed.
  2. Verify the service CLI binary exists, is executable, and its version matches the frontend wrapper's contract.
  3. Route logs to stderr or a file so stdout stays pure JSON.
  4. Read stdout to EOF and, if the CLI emits trailing JSON documents, parse the last complete one instead of the whole stream.

Example fix

// before
const res = parseServiceCliOutput(raw);
// after
let res;
try {
  res = parseServiceCliOutput(raw);
} catch (err) {
  console.error('[cli] raw output was not JSON:', JSON.stringify(raw.slice(0, 500)));
  throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

function looksLikeJson(s: string): boolean {
  const t = s.trim();
  return t.startsWith('{') || t.startsWith('[');
}

if (!looksLikeJson(raw)) {
  throw new Error(`CLI emitted non-JSON stdout: ${raw.slice(0, 120)}`);
}

Try / catch

try {
  const res = parseServiceCliOutput(raw);
} catch (err) {
  if (err instanceof Error && err.message.startsWith('Failed to parse service CLI output as JSON')) {
    console.error('[cli] raw stdout:', JSON.stringify(raw.slice(0, 500)));
    // surface a CLI-level error, not a parse error, to the user
  }
  throw err;
}

Prevention

When it happens

Trigger: The CLI binary prints errors/panics/banner text to stdout instead of pure JSON; RUST_LOG or tracing output pollutes stdout; the command fails at the shell level (missing binary, permission denied) so stdout is empty or carries shell diagnostics; the process is killed mid-write leaving truncated JSON.

Common situations: The service binary is missing from PATH or is a different major version than the wrapper expects; verbose logging configured to write to stdout; concurrent writes interleaving log lines into the JSON document; a half-written pipe read before process exit.

Understand the failure class

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/8bcf8c4ba82460ce. Report an issue: GitHub.