{"record":{"id":"5fcd2ed79af9b1f5","repo":"thedotmack/claude-mem","slug":"sse-stream-returned-http-response-status","errorCode":null,"errorMessage":"SSE stream returned HTTP ${response.status}","messagePattern":"SSE stream returned HTTP (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"openclaw/src/index.ts","lineNumber":550,"sourceCode":"  setConnectionState: (state: ConnectionState) => void,\n  getSourceLabel: (project: string | null | undefined) => string,\n  botToken?: string\n): Promise<void> {\n  let backoffMs = 1000;\n  const maxBackoffMs = 30000;\n\n  while (!abortController.signal.aborted) {\n    try {\n      setConnectionState(\"reconnecting\");\n      api.logger.info(`[claude-mem] Connecting to SSE stream at ${workerBaseUrl(port)}/stream`);\n\n      const response = await fetch(`${workerBaseUrl(port)}/stream`, {\n        signal: abortController.signal,\n        headers: { Accept: \"text/event-stream\" },\n      });\n\n      if (!response.ok) {\n        throw new Error(`SSE stream returned HTTP ${response.status}`);\n      }\n\n      if (!response.body) {\n        throw new Error(\"SSE stream response has no body\");\n      }\n\n      setConnectionState(\"connected\");\n      backoffMs = 1000;\n      api.logger.info(\"[claude-mem] Connected to SSE stream\");\n\n      const reader = response.body.getReader();\n      const decoder = new TextDecoder();\n      let buffer = \"\";\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n","sourceCodeStart":532,"sourceCodeEnd":568,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/openclaw/src/index.ts#L532-L568","documentation":"The OpenClaw plugin's SSE client throws this when the worker's /stream endpoint responds with a non-2xx HTTP status (response.ok is false). It is the first guard in connectToSSEStream before the response body is read. The throw is caught immediately at index.ts:602, logged at warn level, and the loop reconnects with exponential backoff (1s doubling to 30s max), so it is a recoverable connection failure, not a fatal one.","triggerScenarios":"Calling GET ${workerBaseUrl(port)}/stream with Accept: text/event-stream and receiving any status outside 200-299 (e.g. 404 if the worker build lacks the /stream route, 503 if the worker is shutting down, 500 on an unhandled worker exception, or 401/403 if auth middleware is ever added). The check runs on every reconnect attempt inside the while(!aborted) loop.","commonSituations":"Worker not running or crashed on startup (connection refused surfaces as a network error, not this — this fires only when an HTTP response is actually returned). Wrong workerPort in plugin config pointing at a different service that returns non-SSE HTML (404/200-with-HTML can give 404). Worker restarted mid-stream and the first reconnect hits a briefly-not-ready server (503). Mismatched plugin/worker versions where the worker predates the /stream route.","solutions":["Confirm the worker is running and serving /stream: curl -i http://$CLAUDE_MEM_WORKER_HOST:$CLAUDE_MEM_WORKER_PORT/stream and expect HTTP 200 with content-type: text/event-stream.","Verify workerPort plugin config matches the actual worker port (DEFAULT_WORKER_PORT from settings); check api.logger.info line just before the error which prints the exact URL attempted.","Rebuild and restart the worker (npm run build-and-sync or worker:start) so the /stream route and its handlers are current.","If the status is 5xx, inspect worker logs for the upstream exception that produced the error response.","No code change is needed in the plugin — the loop already backs off and reconnects; only act if it never recovers (indicates a persistent worker-side problem)."],"exampleFix":"// before — no fix needed in the plugin; this error is already handled by the reconnect loop at index.ts:602-614\n\n// to silence during intentional worker downtime, abort the controller:\n// abortController.abort();  // breaks the while loop cleanly\n\n// worker side (src/services/worker-service.ts) — ensure /stream is registered and returns 200:\n//   app.get('/stream', (req, res) => { res.setHeader('Content-Type','text/event-stream'); ... })","handlingStrategy":"retry","validationCode":"// Before relying on the stream, confirm the worker endpoint answers 2xx:\nasync function workerStreamReachable(port: number, host: string): Promise<boolean> {\n  try {\n    const res = await fetch(`http://${host}:${port}/stream`, {\n      method: 'GET',\n      headers: { Accept: 'text/event-stream' },\n    });\n    return res.ok && res.body !== null;\n  } catch {\n    return false;\n  }\n}","typeGuard":"function isAbortReason(e: unknown, signal: AbortSignal): boolean {\n  return signal.aborted && e instanceof Error && e.name === 'AbortError';\n}","tryCatchPattern":"// The existing loop at openclaw/src/index.ts:602-614 is the recommended pattern:\n// catch -> if abort, break; else warn + exponential backoff (1s..30s) + retry.\n// No propagation to callers; treat as transient.","preventionTips":["Pin workerPort to the actual worker port in plugin config to avoid hitting a stray service that returns non-SSE responses.","Keep plugin and worker versions in sync so /stream always exists.","Monitor the warn log '[claude-mem] SSE stream error' — persistent retries indicate a worker-side issue worth alerting on."],"tags":["sse","network","worker","openclaw","reconnect"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}