thedotmack/claude-mem · error · ServerClientError

Server ${method} ${path} failed: ${message}

Error message

Server ${method} ${path} failed: ${message}

What it means

Thrown by ServerClient.request when `fetchWithTimeout` itself throws — the request never got an HTTP response. The client inspects the underlying message: if it matches /timed out|timeout/i the kind is `timeout`, otherwise `transport` (DNS failure, ECONNREFUSED, TLS certificate error, socket reset). Both kinds are fallback-eligible, and `cause` carries the original error for diagnosis.

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 e2d1df569a)

Solutions

  1. Verify the server is up and the base URL is right: `curl -i $BASE_URL/v1/health` (or any known route) from the same environment the hooks run in
  2. For ECONNREFUSED, start the server process / fix host and port in the client configuration
  3. For timeout kind, raise the client `timeoutMs` (or HOOK_TIMEOUTS) only after confirming the server is genuinely slow, and check server-side logs for the stall
  4. For TLS failures, fix the certificate or point baseUrl at a URL whose cert validates; as a stopgap honor the fallback path rather than disabling verification
  5. Because transport/timeout are fallback-eligible, catch ServerClientError and degrade to local operation instead of failing the hook

Example fix

// before
await client.recordEvent(input); // Server POST /v1/events failed: fetch failed

// after
try {
  await client.recordEvent(input);
} catch (e) {
  if (isServerClientError(e) && (e.kind === 'transport' || e.kind === 'timeout')) {
    await fallbackToLocal(input); // fallback-eligible by design
  } else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

import { request } from 'undici';
async function serverReachable(baseUrl: string): Promise<boolean> {
  try { await request(baseUrl, { method: 'HEAD', headersTimeout: 2000 }); return true; }
  catch { return false; }
}
if (!(await serverReachable(baseUrl))) throw new Error(`claude-mem server unreachable at ${baseUrl}`);

Type guard

function isTransientServerError(e: unknown): boolean {
  return isServerClientError(e) && (e.kind === 'transport' || e.kind === 'timeout');
}

Try / catch

for (let attempt = 1; attempt <= 3; attempt++) {
  try {
    return await client.recordEvent(input);
  } catch (e) {
    if (!isServerClientError(e) || (e.kind !== 'transport' && e.kind !== 'timeout')) throw e;
    if (attempt === 3) return fallbackToLocal(input); // fallback-eligible kinds
    await sleep(250 * 2 ** (attempt - 1));
  }
}

Prevention

When it happens

Trigger: The configured server base URL is unreachable: server process not running (ECONNREFUSED), wrong host/port in the URL, DNS name not resolving, self-signed/mismatched TLS cert, or the request exceeding `timeoutMs` (default from HOOK_TIMEOUTS.API_REQUEST). Also proxies/firewalls resetting the connection.

Common situations: Hooks configured for server mode while the claude-mem server is down or was never started; base URL pointing at localhost from a container where the server runs on another host; corporate MITM proxies breaking TLS; slow database making the server exceed the hook timeout.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/769c7ed13b6402b4. Report an issue: GitHub.