tinyhumansai/openhuman · error

[transport:lan] HTTP

Error message

[transport:lan] HTTP ${response.status}: ${text || response.statusText}

What it means

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.

Solutions

  1. Re-discover the desktop's current LAN address and re-save the profile (DHCP churn is the top cause)
  2. Check the desktop core's listener posture — if it is loopback-only, LAN profiles cannot work by design
  3. Probe with testCoreRpcConnection(url) (which routes through the shell relay for cleartext LAN URLs) before committing the profile
  4. Match the port to the core's actual RPC port for this session

Example fix

// before
const t = new LanHttpTransport('http://192.168.1.42:9100/rpc');

// after
const url = await rediscoverDesktopRpcUrl(); // resolve current mDNS/IP before constructing
const t = new LanHttpTransport(url);
Defensive patterns

Strategy: validation

Validate before calling

import { testCoreRpcConnection } from '../../coreRpcClient';

async function lanProfileReachable(rpcUrl: string): Promise<boolean> {
  try { return (await testCoreRpcConnection(rpcUrl)).ok; } catch { return false; }
}

Type guard

function isLanHttpError(err: unknown): boolean {
  return err instanceof Error && /^\[transport:lan\] HTTP \d{3}/.test(err.message);
}
function lanStatusOf(err: unknown): number | null {
  const m = err instanceof Error ? err.message.match(/HTTP (\d{3})/) : null;
  return m ? Number(m[1]) : null;
}

Try / catch

try {
  return await transport.call(method, params);
} catch (err) {
  if (lanStatusOf(err) === 404) { await refreshDesktopAddress(); throw err; } // DHCP churn — re-resolve
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/74baaa79f8851231. Report an issue: GitHub.

Appendix: 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 7491200858)