{"record":{"id":"cc39a7bd97153e65","repo":"tinyhumansai/openhuman","slug":"transport-local-response-missing-result","errorCode":null,"errorMessage":"[transport:local] response missing result","messagePattern":"\\[transport:local\\] response missing result","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/services/transport/LocalTransport.ts","lineNumber":87,"sourceCode":"      }\n      throw err;\n    } finally {\n      clearTimeout(timeoutId);\n    }\n\n    if (!response.ok) {\n      const text = await response.text();\n      throw new Error(`[transport:local] HTTP ${response.status}: ${text || response.statusText}`);\n    }\n\n    const json = (await response.json()) as JsonRpcResponse<T>;\n\n    if (json.error) {\n      logErr('[transport:local] ← %s error: %s', method, json.error.message);\n      throw new Error(json.error.message ?? 'Core RPC returned an error');\n    }\n    if (!Object.prototype.hasOwnProperty.call(json, 'result')) {\n      throw new Error('[transport:local] response missing result');\n    }\n\n    log('[transport:local] ← %s id=%d ok', method, id);\n    return json.result as T;\n  }\n\n  async *stream<T>(\n    method: string,\n    params: unknown,\n    opts?: { signal?: AbortSignal }\n  ): AsyncIterable<T> {\n    // Local HTTP doesn't support streaming natively in this project.\n    // Fall back to a single call and yield the result.\n    const result = await this.call<T>(method, params, opts);\n    yield result;\n  }\n\n  async isHealthy(): Promise<boolean> {","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/transport/LocalTransport.ts#L69-L105","documentation":"Identical contract to the LAN variant: LocalTransport requires every 2xx response body to own either `error` or `result`. This fires when http://127.0.0.1:<port>/rpc answers 200 with JSON that has neither — the URL resolved to something that is not the JSON-RPC endpoint, or the response was rewritten in flight.","triggerScenarios":"getCoreRpcUrl() returning the /health endpoint, a stale port now owned by another local service (Vite dev server, another app), a dev proxy (Vite server.proxy) mangling the body, or an old core whose envelope predates strict checks.","commonSituations":"Port collision after core restart picked a new port while the client cached the old one; OPENHUMAN_CORE_REUSE_EXISTING=1 pointing at a manually started core on a nonstandard route; local HTTP debugging proxies (mitmproxy) re-encoding responses.","solutions":["Re-fetch the RPC URL via the core_rpc_url Tauri command rather than caching; the embedded core binds a fresh port per launch","curl the resolved URL with an openhuman.ping POST and confirm a result field comes back","Kill whatever else listens on that port (lsof -i :<port>) or let the core pick its own","Disable any dev proxy rule that intercepts the core port"],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"// Reject non-RPC URLs when wiring the transport:\nfunction assertRpcUrl(url: string): void {\n  if (!/^https?:\\/\\/127\\.0\\.0\\.1:\\d+\\/rpc$/.test(url)) {\n    throw new Error(`suspicious local rpcUrl: ${url}`);\n  }\n}","typeGuard":"function hasResultField(body: unknown): boolean {\n  return typeof body === 'object' && body !== null && Object.prototype.hasOwnProperty.call(body, 'result');\n}","tryCatchPattern":"try { await local.call('openhuman.ping', {}); }\ncatch (err) {\n  if (err instanceof Error && err.message.includes('response missing result')) {\n    // re-resolve rpcUrl via core_rpc_url — the port likely rotated or the URL points at /health\n  } else throw err;\n}","preventionTips":["Derive the rpcUrl only from core_rpc_url, never persist the port","Add a boot-time ping assertion after core start (run_server_embedded_with_ready) before first real call","Keep dev-proxy rules away from the core's loopback port"],"tags":["json-rpc","transport","local-core","response-validation"],"backgroundTag":"json-rpc-invalid-response","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}