tinyhumansai/openhuman · critical

Core returned an empty backend URL

Error message

Core returned an empty backend URL

What it means

Thrown by resolveBackendUrl() when the core RPC 'openhuman.config_resolve_api_url' resolves but yields an empty string in both snake_case (api_url) and camelCase (apiUrl) after trimming. The core derives the backend URL from its config (env/TOML); an empty resolution means the core has no backend URL configured at all, so every subsequent backend API call would target ''. Fails fast here instead of issuing requests to a relative/invalid origin.

Solutions

  1. Set the backend URL for the core: BACKEND_API_URL in .env (then source scripts/load-dotenv.sh and restart) or the backend URL field in Settings.
  2. Restart the core after config changes so config_resolve_api_url re-reads them.
  3. Check the core log at boot for config-load errors (missing/unreadable TOML) that leave the field empty.
  4. If the RPC response is an error envelope rather than a payload, fix the underlying RPC failure first — log the raw response.

Example fix

// before
const resolved = String(response.api_url ?? response.apiUrl ?? '').trim();
if (!resolved) {
  throw new Error('Core returned an empty backend URL');
}

// after — fall back to the web host's own origin in browser dev
const resolved = String(response.api_url ?? response.apiUrl ?? '').trim();
if (!resolved) {
  if (import.meta.env.DEV && window.location.origin.includes('localhost')) {
    return normalizeBaseUrl(window.location.origin); // dev proxy
  }
  throw new Error('Core returned an empty backend URL — set BACKEND_API_URL and restart Core');
}
Defensive patterns

Strategy: validation

Validate before calling

import { callCoreRpc } from './coreRpcClient';

async function backendUrlConfigured(): Promise<boolean> {
  try {
    const r = await callCoreRpc<{ api_url?: string; apiUrl?: string }>({
      method: 'openhuman.config_resolve_api_url',
    });
    return Boolean((r.api_url ?? r.apiUrl ?? '').trim());
  } catch {
    return false;
  }
}
if (!await backendUrlConfigured()) {
  // show setup UI instead of proceeding to backend calls that will fail

Type guard

function hasResolvedApiUrl(res: unknown): res is { api_url?: string; apiUrl?: string } {
  const r = res as { api_url?: unknown; apiUrl?: unknown } | null;
  return Boolean(r) && (typeof r.api_url === 'string' || typeof r.apiUrl === 'string');
}

Try / catch

try {
  url = await resolveBackendUrl();
} catch (e) {
  if (e instanceof Error && e.message === 'Core returned an empty backend URL') {
    openSettings('backend'); // route user to set the backend URL
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Fresh core config with BACKEND_API_URL / backend api_url unset and no default applying; config file missing or unreadable so the field stays empty; the RPC returned an error object (no api_url keys) that String() coerced to ''.

Common situations: Dev setup without sourcing scripts/load-dotenv.sh or copying .env.example; a wiped ~/.openhuman config; running a standalone core whose TOML lacks the backend section; pointing at a core built with a restricted DomainSet that omitted config resolution.

Related errors


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

Appendix: source

Thrown at app/src/services/backendUrl.ts:63

  }

  if (!coreIsTauri()) {
    resolvedBackendUrl = webFallbackBackendUrl();
    return resolvedBackendUrl;
  }

  if (resolvingBackendUrl) {
    return resolvingBackendUrl;
  }

  const generation = backendUrlGeneration;
  resolvingBackendUrl = (async () => {
    const response = await callCoreRpc<{ api_url?: string; apiUrl?: string }>({
      method: 'openhuman.config_resolve_api_url',
    });
    const resolved = String(response.api_url ?? response.apiUrl ?? '').trim();
    if (!resolved) {
      throw new Error('Core returned an empty backend URL');
    }
    const normalized = normalizeBaseUrl(resolved);
    if (generation === backendUrlGeneration) {
      resolvedBackendUrl = normalized;
    }
    return normalized;
  })().finally(() => {
    if (generation === backendUrlGeneration) {
      resolvingBackendUrl = null;
    }
  });

  return resolvingBackendUrl;
}

View on GitHub (pinned to 7491200858)