{"record":{"id":"644e6973b6845add","repo":"thedotmack/claude-mem","slug":"timeout-transport","errorCode":"timeout|transport","errorMessage":"Server ${method} ${path} failed: ${message}","messagePattern":"Server (.+?) (.+?) failed: (.+?)","errorType":"error_code","errorClass":"ServerClientError","httpStatus":null,"severity":"error","filePath":"src/services/hooks/server-client.ts","lineNumber":373,"sourceCode":"    const url = `${this.baseUrl}${path}`;\n    const init: RequestInit = {\n      method,\n      headers: {\n        'Content-Type': 'application/json',\n        Authorization: `Bearer ${this.apiKey}`,\n      },\n    };\n    if (body !== undefined) {\n      init.body = JSON.stringify(body);\n    }\n\n    let response: Response;\n    try {\n      response = await fetchWithTimeout(url, init, this.timeoutMs);\n    } catch (error: unknown) {\n      const message = error instanceof Error ? error.message : String(error);\n      const isTimeout = /timed out|timeout/i.test(message);\n      throw new ServerClientError(\n        isTimeout ? 'timeout' : 'transport',\n        `Server ${method} ${path} failed: ${message}`,\n        { cause: error },\n      );\n    }\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => '');\n      throw new ServerClientError(\n        'http_error',\n        `Server ${method} ${path} returned ${response.status}: ${truncate(text, 200)}`,\n        { status: response.status },\n      );\n    }\n\n    const text = await response.text();\n    if (!text || text.length === 0) {\n      // Endpoints we call always return JSON; a body-less success is unusual","sourceCodeStart":355,"sourceCodeEnd":391,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/services/hooks/server-client.ts#L355-L391","documentation":"When the underlying fetchWithTimeout call to a /v1/* endpoint throws (DNS failure, ECONNREFUSED, socket reset, abort), the catch inspects the error message for 'timed out'/'timeout' to classify it as kind 'timeout' versus a generic 'transport' failure. Both kinds are fallback-eligible, so the hook handler can retry through the worker path. The original error is preserved as cause.","triggerScenarios":"fetchWithTimeout rejects during any ServerClient request: the server is not listening at baseUrl, the network/DNS is unreachable, the request exceeded this.timeoutMs (DEFAULT_TIMEOUT_MS from HOOK_TIMEOUTS.API_REQUEST), a proxy dropped the connection, or TLS handshake failed.","commonSituations":"Server runtime is not running or is on a different host than serverBaseUrl; firewall/proxy blocks outbound traffic; baseUrl has a typo or wrong port; slow server response exceeded the API_REQUEST timeout; transient network blip or DNS hiccup in CI; the server crashed mid-request leaving the socket hanging.","solutions":["Verify the server is reachable: curl -i ${serverBaseUrl}/v1/health (or whichever health route) from the same host running the hooks.","Confirm serverBaseUrl has no trailing path/typo and the port matches the running server.","If the failure is genuinely a timeout, raise CLAUDE_MEM hook timeout config or investigate why the server is slow (DB locks, cold start).","Because this kind is fallback-eligible, let the hook handler catch it via isServerClientError and fall back to the worker path instead of surfacing the error to the user.","Restart the server runtime if it crashed, then retry."],"exampleFix":"// before — caller lets the error propagate\nconst res = await client.recordEvent(input);\n\n// after — catch transport/timeout and fall back\ntry {\n  const res = await client.recordEvent(input);\n} catch (e) {\n  if (e instanceof ServerClientError && e.isFallbackEligible()) {\n    await worker.recordEvent(input); // fallback path\n  } else {\n    throw e;\n  }\n}","handlingStrategy":"retry","validationCode":"async function isServerReachable(baseUrl: string, timeoutMs = 3000): Promise<boolean> {\n  const ctrl = new AbortController();\n  const t = setTimeout(() => ctrl.abort(), timeoutMs);\n  try {\n    const r = await fetch(`${baseUrl.replace(/\\/+$/, '')}/v1/health`, { signal: ctrl.signal });\n    return r.ok || r.status < 500;\n  } catch { return false; } finally { clearTimeout(t); }\n}","typeGuard":"import { ServerClientError } from './server-client.js';\n\nfunction isTransientTransport(e: unknown): boolean {\n  return e instanceof ServerClientError && (e.kind === 'transport' || e.kind === 'timeout');\n}","tryCatchPattern":"for (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    return await client.searchObservations(input);\n  } catch (e) {\n    if (e instanceof ServerClientError && (e.kind === 'transport' || e.kind === 'timeout') && attempt < 2) {\n      await new Promise(r => setTimeout(r, 200 * 2 ** attempt)); // backoff\n      continue;\n    }\n    if (e instanceof ServerClientError && e.isFallbackEligible()) return await worker.search(input);\n    throw e;\n  }\n}","preventionTips":["Run a reachability/health probe before the first real request so transport issues surface early.","Keep per-endpoint timeouts sized to the slowest legitimate response (HOOK_TIMEOUTS.API_REQUEST).","Always branch on isFallbackEligible() so transient transport errors degrade to the worker path."],"tags":["network","transport","timeout","server-client","fallback-eligible"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}