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 unusual

View on GitHub (pinned to d768ba3643)

Solutions

  1. Verify the server is reachable: curl -i ${serverBaseUrl}/v1/health (or whichever health route) from the same host running the hooks.
  2. Confirm serverBaseUrl has no trailing path/typo and the port matches the running server.
  3. If the failure is genuinely a timeout, raise CLAUDE_MEM hook timeout config or investigate why the server is slow (DB locks, cold start).
  4. 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.
  5. 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

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


AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12). Data as JSON: /api/errors/644e6973b6845add. Report an issue: GitHub.