{"record":{"id":"4ba7b20a622d8963","repo":"tinyhumansai/openhuman","slug":"core-rpc-token-unavailable-in-tauri-local-rpc-aut","errorCode":null,"errorMessage":"Core RPC token unavailable in Tauri; local RPC auth cannot be satisfied","messagePattern":"Core RPC token unavailable in Tauri; local RPC auth cannot be satisfied","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/services/coreRpcClient.ts","lineNumber":679,"sourceCode":"  const effectiveTimeoutMs = resolvePerCallTimeoutMs(timeoutMs);\n  const payload: JsonRpcRequestBody = {\n    jsonrpc: '2.0',\n    id: nextJsonRpcId++,\n    method: normalizedMethod,\n    params: params ?? {},\n  };\n\n  try {\n    const [rpcUrl, token] = await Promise.all([getCoreRpcUrl(), getCoreRpcToken()]);\n    coreRpcLog('HTTP request', { id: payload.id, method: payload.method });\n    if (normalizedMethod === 'openhuman.auth_store_session') {\n      coreRpcLog('[rpc] auth_store_session routing', {\n        rpcUrl,\n        tokenSource: getStoredCoreToken() ? 'cloud-stored' : 'local-resolved',\n      });\n    }\n    if (isTauri() && !token) {\n      throw new Error('Core RPC token unavailable in Tauri; local RPC auth cannot be satisfied');\n    }\n\n    const headers: Record<string, string> = { 'Content-Type': 'application/json' };\n    if (token) {\n      headers['Authorization'] = `Bearer ${token}`;\n    }\n    // Bound the fetch. Without this a hung core sidecar would block every\n    // caller (and the UI) forever. We use a manual AbortController +\n    // setTimeout rather than AbortSignal.timeout() so test fake timers can\n    // drive the abort deterministically. Per-call `timeoutMs` (clamped) lets\n    // legitimately-slow RPCs such as first-launch `app_state_snapshot`\n    // (#2156) opt into a longer-but-still-bounded budget.\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => controller.abort(), effectiveTimeoutMs);\n    let response: Response;\n    try {\n      if (isTauri() && rpcUrlNeedsShellRelay(rpcUrl)) {\n        // Self-hosted runtime on a LAN IP — the secure `tauri://localhost`","sourceCodeStart":661,"sourceCodeEnd":697,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/coreRpcClient.ts#L661-L697","documentation":"Thrown by callCoreRpc when running inside the Tauri desktop shell but getCoreRpcToken() resolved to null. The embedded core writes a per-process bearer to ~/.openhuman/core.token at startup, and the Tauri IPC command core_rpc_token reads it; this error means no cloud-mode stored token existed AND the invoke('core_rpc_token') failed or returned empty (e.g. the core process had not booted far enough to write the token yet). Because the local core requires the bearer, the client refuses to send an unauthenticated request rather than guarantee a 401.","triggerScenarios":"Any callCoreRpc(...) fired in Tauri before the embedded core finished bootstrapping (race between renderer RPC and core_process startup); a failed invoke('core_rpc_token') (handler denied/errored, logged as 'failed to load core RPC token'); a core that crashed before writing core.token; note getCoreRpcToken caches the null result (didResolveCoreRpcToken = true), so one early failure sticks for the process lifetime until the token-invalidated event resets it.","commonSituations":"Components that fire RPCs on mount without waiting for the boot/readiness gates; dev flows where the core is restarted (OPENHUMAN_CORE_REUSE_EXISTING=1 pointing at a dead core); a trimmed/absent core.token file in the workspace; tests/mocks that don't stub the core_rpc_token Tauri command while isTauri() is true.","solutions":["Await the app's readiness seam (boot gate / ServiceBlockingGate / core-started event) before issuing callCoreRpc — the token is written by the core at process start","Verify the core process actually launched: check start_core_process / restart_core_process state and that {workspace}/core.token exists on disk","If the failure is cached, trigger token re-resolution (the CORE_RPC_TOKEN_INVALIDATED_EVENT path / invalidate function) and retry the call once","In cloud/remote mode, confirm a stored cloud-mode token exists via getStoredCoreToken() so resolution does not depend on the local sidecar","For unit tests, stub invoke('core_rpc_token') to return a token whenever isTauri() is mocked true"],"exampleFix":"// before\nuseEffect(() => { void callCoreRpc({ method: 'openhuman.platform_health' }); }, []);\n\n// after\nuseEffect(() => {\n  if (!coreReady) return; // gate on core-started / boot state\n  void callCoreRpc({ method: 'openhuman.platform_health' });\n}, [coreReady]);","handlingStrategy":"retry","validationCode":"import { getCoreRpcToken } from '../services/coreRpcClient';\n\nasync function coreAuthReady(): Promise<boolean> {\n  return (await getCoreRpcToken()) !== null; // null in Tauri means this error is certain\n}","typeGuard":"function isTokenUnavailableError(err: unknown): boolean {\n  return err instanceof Error && err.message.includes('Core RPC token unavailable');\n}","tryCatchPattern":"try {\n  await callCoreRpc({ method, params });\n} catch (err) {\n  if (isTokenUnavailableError(err)) {\n    await waitForCoreStarted(); // boot-gate signal; core writes core.token at startup\n    return callCoreRpc({ method, params }); // single retry after token exists\n  }\n  throw err;\n}","preventionTips":["Never fire callCoreRpc before the boot/readiness gate reports the core process started","Stub invoke('core_rpc_token') in any test where isTauri() is true","Remember getCoreRpcToken caches a null resolution — trigger token invalidation after core restart instead of re-calling blindly"],"tags":["tauri","rpc","auth-token","startup-race","core-process"],"backgroundTag":"bearer-token-unavailable","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}