{"record":{"id":"e5c07bab02d04f14","repo":"thedotmack/claude-mem","slug":"http-error","errorCode":"http_error","errorMessage":"Server ${method} ${path} returned ${response.status}: ${truncate(text, 200)}","messagePattern":"Server (.+?) (.+?) returned (.+?): (.+?)","errorType":"error_code","errorClass":"ServerClientError","httpStatus":null,"severity":"error","filePath":"src/services/hooks/server-client.ts","lineNumber":382,"sourceCode":"      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\n      // but not fatal — return undefined-shaped object.\n      return {} as T;\n    }\n    try {\n      return JSON.parse(text) as T;\n    } catch (error: unknown) {\n      const err = error instanceof Error ? error : new Error(String(error));\n      throw new ServerClientError(\n        'invalid_response',","sourceCodeStart":364,"sourceCodeEnd":400,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/services/hooks/server-client.ts#L364-L400","documentation":"After a successful transport round-trip, if response.ok is false (any 4xx/5xx) the client reads the body text (truncated to 200 chars) and throws an http_error ServerClientError carrying the status code. Only 5xx and 429 are fallback-eligible; other 4xx are treated as real client bugs and surfaced so they can be observed rather than silently swallowed.","triggerScenarios":"The server returns 401/403 (bad or expired API key / wrong project scope), 400 (malformed request body, missing projectId), 404 (unknown endpoint or sessionId), 409 (conflict), 429 (rate limited), or any 5xx (server fault). The thrown error's status field mirrors response.status.","commonSituations":"API key is valid-format but revoked or scoped to a different team/project (401/403); caller passed a projectId the key cannot access; recordEvent referenced a serverSessionId that does not exist; server hit an internal error or its database is down (500/503); rate limiter triggered during heavy hook bursts (429).","solutions":["Read the status from the thrown ServerClientError: 401/403 means regenerate/re-scope the API key; 400 means inspect the request payload (projectId, required fields).","For 5xx or 429, treat as transient — the error is fallback-eligible, so route through isFallbackEligible() and use the worker path.","For a persistent 4xx, fix the request shape (e.g. ensure projectId is set and the session was started) before retrying.","Check server logs for the matching request if the truncated body text is not enough to diagnose."],"exampleFix":"// before\nconst res = await client.endSession({ sessionId });\n\n// after — branch on status\ncatch (e) {\n  if (e instanceof ServerClientError && e.kind === 'http_error') {\n    if (e.status === 404) { /* session already ended */ return; }\n    if (e.isFallbackEligible()) { await worker.endSession({ sessionId }); return; }\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// Validate request shape before sending to avoid 400-class http_error.\nfunction assertRecordEvent(input: ServerRecordEventRequest): void {\n  if (!input.projectId) throw new Error('projectId is required');\n  if (!input.eventType) throw new Error('eventType is required');\n  if (typeof input.occurredAtEpoch !== 'number') throw new Error('occurredAtEpoch must be a number');\n}","typeGuard":"import { ServerClientError } from './server-client.js';\n\nfunction isHttpError(e: unknown, status?: number): boolean {\n  return e instanceof ServerClientError && e.kind === 'http_error' && (status === undefined || e.status === status);\n}","tryCatchPattern":"try {\n  return await client.startSession(input);\n} catch (e) {\n  if (e instanceof ServerClientError && e.kind === 'http_error') {\n    if (e.status !== null && (e.status >= 500 || e.status === 429)) {\n      return await worker.startSession(input); // transient -> fallback\n    }\n    // 4xx (non-429) is a real client bug — surface it\n  }\n  throw e;\n}","preventionTips":["Always validate required fields (projectId, eventType) before the call to avoid avoidable 400s.","Distinguish transient (5xx/429) from permanent (4xx) http errors; only fall back on transient.","Rotate API keys proactively to avoid 401/403 storms."],"tags":["http","server-client","authentication","rate-limiting","conditionally-fallback-eligible"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}