{"record":{"id":"2198dbfe59d1ff4c","repo":"tinyhumansai/openhuman","slug":"failed-to-parse-service-cli-output-as-json-parsed","errorCode":null,"errorMessage":"Failed to parse service CLI output as JSON: parsed value does not match CommandResponse shape","messagePattern":"Failed to parse service CLI output as JSON: parsed value does not match CommandResponse shape","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/utils/tauriCommands/common.ts","lineNumber":97,"sourceCode":"    return false;\n  }\n  if (!Array.isArray(candidate.logs)) {\n    return false;\n  }\n  return candidate.logs.every(entry => typeof entry === 'string');\n}\n\nexport function parseServiceCliOutput<T>(raw: string): CommandResponse<T> {\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(raw);\n  } catch (err) {\n    throw new Error(\n      `Failed to parse service CLI output as JSON: ${err instanceof Error ? err.message : String(err)}`\n    );\n  }\n  if (!isCommandResponse<T>(parsed)) {\n    throw new Error(\n      'Failed to parse service CLI output as JSON: parsed value does not match CommandResponse shape'\n    );\n  }\n  return parsed;\n}\n\n/**\n * Typed marker for the CEF \"IPC bridge not wired\" failure mode. The vendored\n * `app/src-tauri/vendor/tauri-cef/crates/tauri/scripts/ipc-protocol.js` falls\n * back to `window.ipc.postMessage(...)` whenever the custom-protocol fetch\n * rejects (network blip, navigation interrupt, mid-session re-entry). On CEF\n * `window.ipc` is never wired — `app/src-tauri/src/cef_impl.rs` drops the\n * `ipc_handler` registration — so the fallback throws\n * `TypeError: Cannot read properties of undefined (reading 'postMessage')`\n * **synchronously**, before the underlying `invoke()` constructs its Promise.\n * The throw escapes the Promise executor and lands on `onunhandledrejection`,\n * which Sentry then captures as `TAURI-REACT-7` / `TAURI-REACT-6` with no user\n * impact recorded because the call sites never caught it.","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/utils/tauriCommands/common.ts#L79-L115","documentation":"parseServiceCliOutput succeeded at JSON.parse but the value failed isCommandResponse: the contract requires a non-array object carrying BOTH a `result` key and a `logs` key, with `logs` an array in which every entry is a string. The CLI emitted valid JSON of a different shape — a bare result payload, an error object, or an envelope from a different contract version.","triggerScenarios":"The CLI subprocess returns valid JSON like `{\"ok\":true}`, a raw result without the `{ result, logs }` envelope, or `{\"error\":\"...\"}` from a failure path that still exits with parseable output; also version skew where a newer/older CLI changed the envelope, or tests feeding sample JSON that omits `logs`.","commonSituations":"App/UI and core CLI built from different commits so the CommandResponse envelope drifted; a CLI failure path that prints its error JSON to stdout with exit code 0; a new subcommand not wrapping output in CommandResponse; fixtures recorded before `logs` was added to the envelope.","solutions":["Log the parsed value's top-level keys to see exactly which shape came back","Align versions: rebuild openhuman-core (or the shell's bundled core) to match the UI expecting `{ result, logs }`","Check the CLI's exit status separately — an error envelope on stdout usually accompanies a non-zero exit and should be handled as a command failure, not parsed as a response","If the contract legitimately changed, update CommandResponse/isCommandResponse in tauriCommands/common.ts in the same commit"],"exampleFix":"// before\nconst resp = parseServiceCliOutput(raw); // throws on {\"error\":\"boom\"} with exit 0\n\n// after (branch on exit code before parsing the success envelope)\nif (exitCode !== 0) {\n  throw new Error(`core CLI failed (exit ${exitCode}): ${raw}`);\n}\nconst resp = parseServiceCliOutput(raw);","handlingStrategy":"type-guard","validationCode":"const matchesEnvelope = (raw: string): boolean => {\n  try {\n    const v = JSON.parse(raw) as { result?: unknown; logs?: unknown };\n    return !!v && typeof v === 'object' && !Array.isArray(v)\n      && 'result' in v && 'logs' in v\n      && Array.isArray(v.logs) && v.logs.every(e => typeof e === 'string');\n  } catch {\n    return false;\n  }\n};\n\nif (!matchesEnvelope(raw)) {\n  // different shape (e.g. {error: ...}) — handle as a command failure\n  throw new Error(`Unexpected core CLI payload: ${raw.slice(0, 200)}`);\n}","typeGuard":"function isCommandResponse<T>(value: unknown): value is CommandResponse<T> {\n  if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n  const c = value as { result?: unknown; logs?: unknown };\n  return 'result' in c && 'logs' in c\n    && Array.isArray(c.logs)\n    && c.logs.every(e => typeof e === 'string');\n}","tryCatchPattern":"try {\n  const resp = parseServiceCliOutput<MyResult>(raw);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('CommandResponse shape')) {\n    // Valid JSON, wrong contract — check exit code and version skew first\n    await verifyCoreCliVersion();\n    throw err;\n  }\n  throw err;\n}","preventionTips":["Version-lock the core CLI and the UI so the { result, logs } envelope cannot drift","Check the subprocess exit code before parsing — error envelopes on stdout usually pair with non-zero exits","When the envelope contract changes, update isCommandResponse and all fixtures in the same commit"],"tags":["cli","json","validation","contract","tauri"],"backgroundTag":"response-schema-validation-failed","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}