tinyhumansai/openhuman · error
[transport:lan] HTTP ${response.status}: ${text || response.
Error message
[transport:lan] HTTP ${response.status}: ${text || response.statusText} What it means
The LAN peer answered with a non-2xx HTTP status; the transport embeds status plus response body (or statusText). Because LAN connections send only `Content-Type` — the Authorization header is intentionally absent, trust is network-level — a 401 here means the target is NOT the expected desktop core but something that demands auth (a router portal, another service on that port).
Source
Thrown at app/src/services/transport/LanHttpTransport.ts:69
try {
response = await fetch(this.rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});
} catch (err) {
if (controller.signal.aborted) {
throw new Error(`[transport:lan] ${method} timed out after ${this.timeoutMs}ms`);
}
throw err;
} finally {
clearTimeout(timeoutId);
}
if (!response.ok) {
const text = await response.text();
throw new Error(`[transport:lan] HTTP ${response.status}: ${text || response.statusText}`);
}
const json = (await response.json()) as JsonRpcResponse<T>;
if (json.error) {
logErr('[transport:lan] ← %s error: %s', method, json.error.message);
throw new Error(json.error.message ?? 'LAN RPC returned an error');
}
if (!Object.prototype.hasOwnProperty.call(json, 'result')) {
throw new Error('[transport:lan] response missing result');
}
log('[transport:lan] ← %s id=%d ok', method, id);
return json.result as T;
}
async *stream<T>(
method: string,View on GitHub (pinned to a221052e0d)
Solutions
- Read the embedded status/body — it identifies what actually answered
- Re-resolve the desktop's current IP:port and update the connection profile (re-pair)
- Open the rpcUrl in a browser on the client device: the core's `GET /health` should answer
- Remove any middlebox (proxy/portal) between client and desktop, or configure it to pass POST bodies untouched
Defensive patterns
Strategy: try-catch
Validate before calling
const u = new URL(profile.rpcUrl);
if (!u.pathname.endsWith('/rpc')) {
throw new Error('LAN profile rpcUrl must point at the core /rpc path');
} Type guard
function lanHttpStatus(e: unknown): number | null {
if (!(e instanceof Error)) return null;
const m = /^\[transport:lan\] HTTP (\d{3}):/.exec(e.message);
return m ? Number(m[1]) : null;
} Try / catch
try { await lan.call(m, p); }
catch (e) {
const status = lanHttpStatus(e);
if (status === 401 || status === 403) promptRePair(); // not our core on that port
else if (status === 404) refreshProfileUrl();
else throw e;
} Prevention
- A 401 on LAN means the wrong host answered (LAN core sends no auth) — treat it as a pairing problem
- Re-pair after the desktop's network changes instead of hand-editing IPs
- Parse the embedded status code from the message to branch recovery logic
When it happens
Trigger: 404: stale rpcUrl path after the desktop's port changed; 401/403: reached some other authenticated service occupying the port; 400 from a strict middleware rejecting the JSON-RPC frame; 5xx from whatever actually listens there.
Common situations: DHCP gave the desktop a new IP and the old one now hosts a different device; port forwarding/reverse proxy in front of the core adding auth; the profile was hand-edited with a wrong port; captive portal on public Wi-Fi intercepting with a 3xx→200 HTML splash (surfaces as HTTP 200 + parse errors instead).
Related errors
- [transport:cloud] HTTP ${response.status}: ${text || respons
- [transport:lan] ${method} timed out after ${this.timeoutMs}m
- [transport:manager] lan profile missing rpcUrl
- Failed to send magic link (${response.status})
- HTTP error! status: ${response.status}
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/74baaa79f8851231.
Report an issue: GitHub.