{"record":{"id":"298edcce806af82f","repo":"tinyhumansai/openhuman","slug":"transport-lan-response-missing-result","errorCode":null,"errorMessage":"[transport:lan] response missing result","messagePattern":"\\[transport:lan\\] response missing result","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/services/transport/LanHttpTransport.ts","lineNumber":79,"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:lan] 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:lan] ← %s error: %s', method, json.error.message);\n      throw new Error(json.error.message ?? 'LAN RPC returned an error');\n    }\n    if (!Object.prototype.hasOwnProperty.call(json, 'result')) {\n      throw new Error('[transport:lan] response missing result');\n    }\n\n    log('[transport:lan] ← %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    const result = await this.call<T>(method, params, opts);\n    yield result;\n  }\n\n  async isHealthy(): Promise<boolean> {\n    try {\n      await this.call('openhuman.ping', {}, { signal: AbortSignal.timeout(2000) });","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/transport/LanHttpTransport.ts#L61-L97","documentation":"LanHttpTransport.call() POSTs a JSON-RPC 2.0 body to the profile's rpcUrl and requires every 2xx response to carry either an `error` or an own-property `result`. This error means the body parsed as JSON but contained neither — the endpoint answered with something that is not a JSON-RPC response. It fires only after the HTTP-status check and the `json.error` check both passed, so the server returned 200 with a JSON object that has no result field.","triggerScenarios":"A LAN rpcUrl that points at a non-RPC JSON endpoint (e.g. the core's GET /health returning {\"status\":\"ok\"} instead of POST /rpc); a reverse proxy or captive portal injecting its own JSON status object; the remote host running an older core binary whose response shape predates the strict result check; a port-forward landing on a different HTTP service.","commonSituations":"Typo'd URL when adding the LAN host (path /health or / instead of /rpc), router port-forward misconfiguration, firewall intercepting with a JSON portal page, or a core/agent version skew between the desktop host and this client.","solutions":["Point the LAN profile's rpcUrl at the JSON-RPC endpoint, e.g. http://<lan-host>:<port>/rpc, not /health or /","Verify with curl -X POST <rpcUrl> -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"openhuman.ping\",\"params\":{}}' and confirm the reply contains \"result\"","Restart openhuman-core on the host (./target/debug/openhuman-core serve) so a fresh RPC listener is up, then re-enter the LAN URL","Remove any HTTP proxy between client and LAN host, or add an exception for the core port"],"exampleFix":"// before\nconst profile = { kind: 'lan', rpcUrl: 'http://192.168.1.20:5800/health' };\n// after\nconst profile = { kind: 'lan', rpcUrl: 'http://192.168.1.20:5800/rpc' };","handlingStrategy":"type-guard","validationCode":"// Before trusting a LAN rpcUrl, confirm it speaks JSON-RPC:\nasync function isJsonRpcEndpoint(rpcUrl: string): Promise<boolean> {\n  const res = await fetch(rpcUrl, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'openhuman.ping', params: {} }),\n  });\n  if (!res.ok) return false;\n  const body = await res.json().catch(() => null);\n  return !!body && typeof body === 'object' && ('result' in body || 'error' in body);\n}","typeGuard":"function isJsonRpcSuccess<T>(body: unknown): body is { result: T } {\n  return (\n    typeof body === 'object' && body !== null &&\n    !('error' in body) &&\n    Object.prototype.hasOwnProperty.call(body, 'result')\n  );\n}","tryCatchPattern":"try {\n  await lanTransport.call('openhuman.ping', {});\n} catch (err) {\n  if (err instanceof Error && err.message === '[transport:lan] response missing result') {\n    // endpoint is not speaking JSON-RPC — fix rpcUrl, do not retry blindly\n  }\n  throw err;\n}","preventionTips":["Always store the full /rpc path in LAN profiles, never the bare host or /health","Health-check with an openhuman.ping POST when a LAN profile is added, before persisting it","Prefer type-guarding the parsed envelope over trusting any 200 response"],"tags":["json-rpc","transport","lan","response-validation","network"],"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"}