{"record":{"id":"9cd46846cdb8114e","repo":"tinyhumansai/openhuman","slug":"transport-lan-method-timed-out-after-this-t","errorCode":null,"errorMessage":"[transport:lan] ${method} timed out after ${this.timeoutMs}ms","messagePattern":"\\[transport:lan\\] (.+?) timed out after (.+?)ms","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/services/transport/LanHttpTransport.ts","lineNumber":60,"sourceCode":"    const payload: JsonRpcRequestBody = { jsonrpc: '2.0', id, method, params: params ?? {} };\n\n    log('[transport:lan] → %s id=%d', method, id);\n\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);\n    opts?.signal?.addEventListener('abort', () => controller.abort());\n\n    let response: Response;\n    try {\n      response = await fetch(this.rpcUrl, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(payload),\n        signal: controller.signal,\n      });\n    } catch (err) {\n      if (controller.signal.aborted) {\n        throw new Error(`[transport:lan] ${method} timed out after ${this.timeoutMs}ms`);\n      }\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')) {","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/services/transport/LanHttpTransport.ts#L42-L78","documentation":"LAN-transport timeout: `fetch` rejected and the shared controller was aborted — by the internal timer (default 10s, deliberately shorter than cloud/local) or by the caller's signal, which the `controller.signal.aborted` check cannot distinguish. This transport connects a remote client (e.g. the iOS app) to the desktop core over the local network with no Authorization header (network-level trust per the class docs), so a timeout almost always means the desktop core was unreachable or slow.","triggerScenarios":"iOS device and desktop on different networks/VLANs; desktop core not running or its listener bound to loopback only; phone Wi-Fi with poor signal to the desktop host; the 10s default being too tight for a heavy method over Wi-Fi; caller cancellation mislabeled as timeout.","commonSituations":"Connection profile's rpcUrl host/port stale after the desktop's IP changed (DHCP); firewall on the desktop blocking the port; desktop asleep; user switched from home to public Wi-Fi which isolates clients.","solutions":["Verify both devices are on the same network and the desktop core is running (its URL answers `GET /health`)","Re-pair/re-fetch the connection profile so rpcUrl matches the desktop's current IP:port","Allow the port through the desktop firewall; ensure the core's listener posture covers LAN (a deliberate user decision, not default)","Raise `timeoutMs` in the LanHttpTransport constructor for known-slow calls, and retry idempotent ones with backoff"],"exampleFix":"// before\nconst t = new LanHttpTransport(profile.rpcUrl); // 10s default\nawait t.call('openhuman.memory_search', q);\n\n// after\nconst t = new LanHttpTransport(profile.rpcUrl, 30_000);\nawait withRetry(() => t.call('openhuman.memory_search', q), { tries: 3 });","handlingStrategy":"retry","validationCode":"async function lanReachable(rpcUrl: string): Promise<boolean> {\n  try {\n    const r = await fetch(new URL('/health', rpcUrl).toString(), { signal: AbortSignal.timeout(3000) });\n    return r.ok;\n  } catch { return false; }\n}","typeGuard":"function isLanTimeout(e: unknown): boolean {\n  return e instanceof Error && e.message.startsWith('[transport:lan]') && e.message.includes('timed out');\n}","tryCatchPattern":"if (!(await lanReachable(profile.rpcUrl))) {\n  throw new Error('Desktop core unreachable — check both devices share the network.');\n}\nfor (let a = 1; a <= 3; a++) {\n  try { return await lan.call(m, p); }\n  catch (e) { if (!isLanTimeout(e) || a === 3) throw e; await sleep(2 ** a * 300); }\n}","preventionTips":["Prefer stable hostnames/mDNS over raw IPs in LAN connection profiles to survive DHCP changes","Health-check the desktop core before first RPC of a session","Remember the 10s default is the shortest of the three transports — raise it for heavy methods","Caller-signal aborts are reported as timeouts; check your own cancellation first"],"tags":["network","timeout","lan","transport","ios"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}