thedotmack/claude-mem · error · ServerClientError
timeout|transport
timeout|transport
Error message
Server ${method} ${path} failed: ${message} What it means
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.
Source
Thrown at src/services/hooks/server-client.ts:373
const url = `${this.baseUrl}${path}`;
const init: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
};
if (body !== undefined) {
init.body = JSON.stringify(body);
}
let response: Response;
try {
response = await fetchWithTimeout(url, init, this.timeoutMs);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const isTimeout = /timed out|timeout/i.test(message);
throw new ServerClientError(
isTimeout ? 'timeout' : 'transport',
`Server ${method} ${path} failed: ${message}`,
{ cause: error },
);
}
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new ServerClientError(
'http_error',
`Server ${method} ${path} returned ${response.status}: ${truncate(text, 200)}`,
{ status: response.status },
);
}
const text = await response.text();
if (!text || text.length === 0) {
// Endpoints we call always return JSON; a body-less success is unusualView on GitHub (pinned to d768ba3643)
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.
Example fix
// before — caller lets the error propagate
const res = await client.recordEvent(input);
// after — catch transport/timeout and fall back
try {
const res = await client.recordEvent(input);
} catch (e) {
if (e instanceof ServerClientError && e.isFallbackEligible()) {
await worker.recordEvent(input); // fallback path
} else {
throw e;
}
} Defensive patterns
Strategy: retry
Validate before calling
async function isServerReachable(baseUrl: string, timeoutMs = 3000): Promise<boolean> {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const r = await fetch(`${baseUrl.replace(/\/+$/, '')}/v1/health`, { signal: ctrl.signal });
return r.ok || r.status < 500;
} catch { return false; } finally { clearTimeout(t); }
} Type guard
import { ServerClientError } from './server-client.js';
function isTransientTransport(e: unknown): boolean {
return e instanceof ServerClientError && (e.kind === 'transport' || e.kind === 'timeout');
} Try / catch
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await client.searchObservations(input);
} catch (e) {
if (e instanceof ServerClientError && (e.kind === 'transport' || e.kind === 'timeout') && attempt < 2) {
await new Promise(r => setTimeout(r, 200 * 2 ** attempt)); // backoff
continue;
}
if (e instanceof ServerClientError && e.isFallbackEligible()) return await worker.search(input);
throw e;
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ${timeoutMessage} (timed out after ${timeoutMs}ms)
- Worker request timed out after ${WORKER_FETCH_TIMEOUT_MS}ms:
- missing_api_key
- Request timed out after ${timeoutMs}ms
- SSE stream returned HTTP ${response.status}
AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12).
Data as JSON: /api/errors/644e6973b6845add.
Report an issue: GitHub.