{"record":{"id":"689c13cad640a0a6","repo":"tinyhumansai/openhuman","slug":"core-rpc-payload-method-timed-out-after-effec","errorCode":null,"errorMessage":"Core RPC ${payload.method} timed out after ${effectiveTimeoutMs}ms","messagePattern":"Core RPC (.+?) timed out after (.+?)ms","errorType":"exception","errorClass":"CoreRpcError","httpStatus":null,"severity":"error","filePath":"app/src/services/coreRpcClient.ts","lineNumber":726,"sourceCode":"          JSON.stringify(payload),\n          controller.signal\n        );\n      } else {\n        response = await fetch(rpcUrl, {\n          method: 'POST',\n          headers,\n          body: JSON.stringify(payload),\n          signal: controller.signal,\n        });\n      }\n    } catch (fetchErr) {\n      if (controller.signal.aborted) {\n        // Throw a fully-classified `CoreRpcError` here so the outer catch\n        // doesn't re-wrap a bare `Error` and so callers can branch on\n        // `err.kind === 'timeout'` (Sentry filter, soft toast skip). Use\n        // the per-call `effectiveTimeoutMs` so the message reflects the\n        // actual budget (#2156 raised the snapshot path to 90s).\n        throw new CoreRpcError(\n          `Core RPC ${payload.method} timed out after ${effectiveTimeoutMs}ms`,\n          'timeout'\n        );\n      }\n      throw fetchErr;\n    } finally {\n      clearTimeout(timeoutId);\n    }\n\n    if (!response.ok) {\n      const text = await response.text();\n      const httpMessage = `Core RPC HTTP ${response.status}: ${text || response.statusText}`;\n      const kind = classifyRpcError(text || response.statusText, response.status);\n      if (kind === 'auth_expired' && !suppressAuthExpiredEvent)\n        dispatchAuthExpired(\n          payload.method,\n          classifyAuthExpiredReason(text || response.statusText, response.status)\n        );","sourceCodeStart":708,"sourceCodeEnd":744,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/coreRpcClient.ts#L708-L744","documentation":"A fully-classified CoreRpcError with kind 'timeout': the AbortController that bounds every core RPC fetch fired before the response arrived. The budget is effectiveTimeoutMs = resolvePerCallTimeoutMs(timeoutMs) — the default CORE_RPC_TIMEOUT_MS unless the caller passes a per-call override, clamped to 1s..10min. It is deliberately a CoreRpcError (not a bare Error) so the outer catch does not re-wrap it and callers can branch on err.kind === 'timeout' (Sentry filtering, soft-toast skipping).","triggerScenarios":"Core takes longer than the budget: a hung/deadlocked core process, first-launch app_state_snapshot with cold stores (#2156 raised that path to 90s via timeoutMs), heavy migrations on boot, or a too-low per-call timeoutMs being clamped. Any callCoreRpc(method) where the core does not answer within effectiveTimeoutMs.","commonSituations":"Cold-start workloads on large workspaces; a core blocked on a lock or stuck subprocess; dev machines with slow disks; passing timeoutMs > 600000 silently clamping to 10min and still timing out; network relay path (shell relay for LAN URLs) adding latency.","solutions":["Pass a per-call budget for known-slow RPCs: callCoreRpc({ method, params, timeoutMs: 90_000 }) — mirroring the app_state_snapshot fix in #2156","Check whether the core process is alive and responsive (health endpoint / platform health RPC) and restart it if hung","Branch on err.kind === 'timeout' to retry idempotent reads once instead of surfacing a hard error","Inspect core-side logs to find what made the handler exceed the default budget (lock contention, migration, cold cache)"],"exampleFix":"// before\nconst snap = await callCoreRpc<AppStateSnapshot>({ method: 'openhuman.app_state_snapshot' });\n\n// after\nconst snap = await callCoreRpc<AppStateSnapshot>({\n  method: 'openhuman.app_state_snapshot',\n  timeoutMs: 90_000, // first-launch snapshot budget (#2156)\n});","handlingStrategy":"retry","validationCode":null,"typeGuard":"import { CoreRpcError } from '../services/coreRpcClient';\n\nfunction isCoreRpcTimeout(err: unknown): boolean {\n  return err instanceof CoreRpcError && err.kind === 'timeout';\n}","tryCatchPattern":"try {\n  return await callCoreRpc({ method, params, timeoutMs: 90_000 });\n} catch (err) {\n  if (isCoreRpcTimeout(err) && isIdempotentRead(method)) {\n    return await callCoreRpc({ method, params, timeoutMs: 90_000 }); // one bounded retry\n  }\n  throw err;\n}","preventionTips":["Pass explicit timeoutMs for known-slow RPCs (cold snapshots, migrations) instead of relying on the default","Branch on err.kind === 'timeout' in telemetry filters so soft timeouts don't page anyone","Watch core health: persistent timeouts usually mean a hung core, not a small budget"],"tags":["rpc","timeout","abort-controller","core-process","slow-request"],"backgroundTag":"request-timeout","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}