tinyhumansai/openhuman · error
LAN RPC returned an error
Error message
LAN RPC returned an error
What it means
The LAN core answered 2xx with a JSON-RPC error envelope whose message was null/undefined, so the generic 'LAN RPC returned an error' text is used. The debug line ('[transport:lan] ← %s error') logged json.error.message just before, and json.error.code is available on the wire frame even though this plain Error drops it.
Solutions
- Enable DEBUG='transport:lan:error' to capture the logged json.error.message/code pair
- Check the desktop core's /schema for the method before the mobile UI calls it
- Update the desktop app so both ends share the method surface
- Gate UI features on a capability/schema probe rather than assuming method availability
Example fix
// before
const r = await transport.call('openhuman.meet_list_upcoming', {});
// after
const schema = await transport.call<{ methods: string[] }>('core.schema', {});
if (!schema.methods.includes('openhuman.meet_list_upcoming')) throw new Error('meet not available on this core');
const r = await transport.call('openhuman.meet_list_upcoming', {}); Defensive patterns
Strategy: try-catch
Validate before calling
async function lanCoreHasMethod(transport: LanHttpTransport, method: string): Promise<boolean> {
const schema = await transport.call<{ methods: string[] }>('core.schema', {});
return schema.methods.includes(method);
} Type guard
function isLanEnvelopeError(err: unknown): boolean {
return err instanceof Error && (err.message === 'LAN RPC returned an error' || err.message.startsWith('[transport:lan]'));
} Try / catch
try {
return await transport.call<T>(method, params);
} catch (err) {
if (isLanEnvelopeError(err)) {
// DEBUG='transport:lan:error' logged json.error.code/message — correlate before reporting
logger.error('LAN core rejected call', { method });
}
throw err;
} Prevention
- Schema-probe the desktop core before calling domain methods from mobile UI
- Keep the desktop app updated so both ends share the method surface
- Capture the logged json.error.code — the thrown message intentionally omits it
When it happens
Trigger: Calling a method the desktop core doesn't register while on a LAN profile (gated domain off in that core build, or app/core version skew); server-side validation failures whose handler omits the message field.
Common situations: iOS client newer than the desktop build (method added later); desktop running a slim feature set (--no-default-features) that omitted a family the mobile UI expects; drifted method names between versions.
Related errors
- Cloud RPC returned an error
- Core RPC returned an error
- [transport:lan] response missing result
- Core RPC returned an error
- MCP HTTP
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/16485feffbff1998.
Report an issue: GitHub.
Appendix: source
Thrown at app/src/services/transport/LanHttpTransport.ts:76
} 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,
params: unknown,
opts?: { signal?: AbortSignal }
): AsyncIterable<T> {
const result = await this.call<T>(method, params, opts);
yield result;
}
View on GitHub (pinned to 7491200858)