{"record":{"id":"74baaa79f8851231","repo":"tinyhumansai/openhuman","slug":"transport-lan-http-response-status-text","errorCode":null,"errorMessage":"[transport:lan] HTTP ${response.status}: ${text || response.statusText}","messagePattern":"\\[transport:lan\\] HTTP (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/services/transport/LanHttpTransport.ts","lineNumber":69,"sourceCode":"    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')) {\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,","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/transport/LanHttpTransport.ts#L51-L87","documentation":"The LAN transport's POST to the profile's rpcUrl returned non-2xx; body text (or statusText) is inlined with the status. No classification, no session-expiry handling — a plain Error. Since LAN transports send no Authorization header, 401/403 here means the remote core's network/session trust rejected the request, not a stale bearer.","triggerScenarios":"403 from a desktop core that doesn't trust the LAN peer; 404 from a stale IP/port in the connection profile (DHCP renewal changed the desktop's address); 502/503 from intermediate tooling; connection refused variants surfacing as gateway errors.","commonSituations":"Profile saved with last week's LAN IP; desktop core bound to loopback only while the profile assumes LAN exposure; port changed after a core restart; phone switched networks (guest Wi-Fi isolating clients).","solutions":["Re-discover the desktop's current LAN address and re-save the profile (DHCP churn is the top cause)","Check the desktop core's listener posture — if it is loopback-only, LAN profiles cannot work by design","Probe with testCoreRpcConnection(url) (which routes through the shell relay for cleartext LAN URLs) before committing the profile","Match the port to the core's actual RPC port for this session"],"exampleFix":"// before\nconst t = new LanHttpTransport('http://192.168.1.42:9100/rpc');\n\n// after\nconst url = await rediscoverDesktopRpcUrl(); // resolve current mDNS/IP before constructing\nconst t = new LanHttpTransport(url);","handlingStrategy":"validation","validationCode":"import { testCoreRpcConnection } from '../../coreRpcClient';\n\nasync function lanProfileReachable(rpcUrl: string): Promise<boolean> {\n  try { return (await testCoreRpcConnection(rpcUrl)).ok; } catch { return false; }\n}","typeGuard":"function isLanHttpError(err: unknown): boolean {\n  return err instanceof Error && /^\\[transport:lan\\] HTTP \\d{3}/.test(err.message);\n}\nfunction lanStatusOf(err: unknown): number | null {\n  const m = err instanceof Error ? err.message.match(/HTTP (\\d{3})/) : null;\n  return m ? Number(m[1]) : null;\n}","tryCatchPattern":"try {\n  return await transport.call(method, params);\n} catch (err) {\n  if (lanStatusOf(err) === 404) { await refreshDesktopAddress(); throw err; } // DHCP churn — re-resolve\n  throw err;\n}","preventionTips":["Re-validate the profile URL whenever the network changes (SSID switch, wake from sleep)","Remember LAN transport sends no auth header — trust is network-level; 403 means posture/allowlist config","Prefer mDNS/stable hostnames over raw IPs in saved profiles"],"tags":["transport","lan","http-status","connection-profile","dhcp"],"backgroundTag":"http-error-response","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}