{"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":"Error","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/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/utils/tauriCommands/common.ts#L79-L115","documentation":"The sibling of the JSON.parse failure in parseServiceCliOutput(): stdout parsed as valid JSON, but isCommandResponse() — the structural check for the CommandResponse envelope, including that `logs` is an array of strings — rejected it. The CLI spoke JSON, just not the envelope this wrapper expects; typically a version mismatch between the CLI binary and the frontend types, or a bare value emitted where the `{ success, data, logs }` envelope was assumed.","triggerScenarios":"The CLI outputs a bare result object without the envelope; the envelope schema changed across versions (renamed/missing `logs`, non-string log entries); an error object was printed as JSON on the success path; a test fixture omits `logs`.","commonSituations":"Frontend updated ahead of the bundled core binary (or vice versa) after an envelope change; a new CLI version serializes RpcOutcome without logs; hand-written test fixtures that never matched the real shape.","solutions":["Log the parsed value and diff it against isCommandResponse's requirements (notably `logs` must exist and be an array of strings).","Rebuild/reinstall the matching service CLI binary so the envelope shape matches the frontend wrapper.","If the contract legitimately changed, update isCommandResponse and the CommandResponse type together in the same commit.","Make the CLI always wrap bare values into the envelope instead of emitting them raw."],"exampleFix":"// before\nconst res = parseServiceCliOutput<T>(raw);\n// after\nconst parsed = JSON.parse(raw) as unknown;\nconst res = isCommandResponse<T>(parsed)\n  ? parsed\n  : ({ success: true, data: parsed, logs: [] } as CommandResponse<T>); // tolerate bare values","handlingStrategy":"type-guard","validationCode":"const parsed: unknown = JSON.parse(raw);\nif (!isCommandResponse(parsed)) {\n  // version drift or bare value — decide explicitly instead of letting parse throw\n}","typeGuard":"function isCommandResponse<T>(v: unknown): v is CommandResponse<T> {\n  if (typeof v !== 'object' || v === null) return false;\n  const c = v as Record<string, unknown>;\n  return (\n    typeof c.success === 'boolean' &&\n    Array.isArray(c.logs) &&\n    c.logs.every((l) => typeof l === 'string')\n  );\n}","tryCatchPattern":"try {\n  const res = parseServiceCliOutput<MyResult>(raw);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('does not match CommandResponse shape')) {\n    // log parsed shape + raw, then align CLI version or update the guard\n  }\n  throw err;\n}","preventionTips":["Ship CLI and frontend wrapper changes to the envelope in the same commit.","Add fixtures for both the envelope and bare-value forms to parse tests.","When the contract changes, update isCommandResponse and CommandResponse together."],"tags":["json","validation","cli","schema-drift"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}