{"record":{"id":"8edcf829d704bf2b","repo":"can1357/oh-my-pi","slug":"auth-broker-stream-ended-unexpectedly","errorCode":null,"errorMessage":"Auth broker stream ended unexpectedly","messagePattern":"Auth broker stream ended unexpectedly","errorType":"exception","errorClass":"AuthBrokerError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/auth-broker/client.ts","lineNumber":262,"sourceCode":"\t\t\t\t});\n\t\t\t}\n\t\t\tconst validated = snapshotStreamEventSchema(parsed);\n\t\t\tif (validated instanceof type.errors) {\n\t\t\t\tthrow new AuthBrokerError(\"Auth broker stream event failed schema validation\", {\n\t\t\t\t\tbody: validated.summary,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst event = validated as SnapshotStreamEvent;\n\t\t\tif (!sawFirstEvent) {\n\t\t\t\tsawFirstEvent = true;\n\t\t\t\tif (event.kind !== \"snapshot\") {\n\t\t\t\t\tthrow new AuthBrokerError(\"Auth broker stream did not start with snapshot\", { body: sse.data });\n\t\t\t\t}\n\t\t\t}\n\t\t\tyield event;\n\t\t}\n\t\tif (!opts.signal?.aborted) {\n\t\t\tthrow new AuthBrokerError(\n\t\t\t\tsawFirstEvent\n\t\t\t\t\t? \"Auth broker stream ended unexpectedly\"\n\t\t\t\t\t: \"Auth broker stream ended before initial snapshot\",\n\t\t\t\t{ status: response.status },\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Fetch aggregate broker usage with a timeout sized for serialized\n\t * same-provider account probes.\n\t */\n\tfetchUsage(options: { signal?: AbortSignal; maxAccountsPerProvider?: number } = {}): Promise<UsageResponse> {\n\t\tconst requestedAccountCount = options.maxAccountsPerProvider;\n\t\tconst accountCount =\n\t\t\ttypeof requestedAccountCount === \"number\" && Number.isFinite(requestedAccountCount)\n\t\t\t\t? Math.max(1, Math.floor(requestedAccountCount))\n\t\t\t\t: 1;","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/auth-broker/client.ts#L244-L280","documentation":"After the SSE snapshot stream yields at least one event and the event loop terminates while the caller's AbortSignal was NOT aborted, openSnapshotStream throws \"Auth broker stream ended unexpectedly\" (client.ts:261-268). The stream is designed to be long-lived; the server closing it (or the connection dropping) after startup is a protocol failure, not a normal completion, because the client was never signalled to stop. This distinguishes server-initiated termination from intentional caller cancellation.","triggerScenarios":"Iterating openSnapshotStream() where: the first event was received, then the broker closes the connection or the TCP/HTTP2 connection drops, and opts.signal is not aborted. Emitted from the generator after readSseEvents exhausts response.body.","commonSituations":"Broker process restart or crash mid-stream; idle connection reaped by an intermediate NAT, load balancer, or reverse proxy (e.g. nginx proxy_read_timeout); network interruption on a laptop switching Wi-Fi; broker deploy rolling out a new version while clients hold open streams.","solutions":["Wrap the openSnapshotStream iteration in a reconnect loop with backoff — the client intentionally does not auto-reconnect, so the caller owns reconnection.","Check broker and proxy idle/keepalive timeouts (e.g. nginx proxy_read_timeout, cloud LB idle timeout) and raise them above your keepalive interval.","Verify the broker didn't crash: check broker logs for panics/OOM around the disconnect time.","Ensure the caller's signal isn't aborted by another component (an abort here would silently end the loop instead of throwing)."],"exampleFix":"// before\nfor await (const event of client.openSnapshotStream({ signal })) {\n  applyEvent(event);\n}\n\n// after\nwhile (!signal.aborted) {\n  try {\n    for await (const event of client.openSnapshotStream({ signal })) {\n      applyEvent(event);\n    }\n    if (!signal.aborted) logger.warn(\"snapshot stream ended, reconnecting\");\n  } catch (err) {\n    if (signal.aborted) break;\n    logger.warn(\"snapshot stream error, retrying\", { error: err });\n  }\n  await Bun.sleep(backoffMs); // exponential backoff\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"function isStreamEndedUnexpectedly(err: unknown): err is AuthBrokerError {\n  return err instanceof AuthBrokerError && err.message === \"Auth broker stream ended unexpectedly\";\n}","tryCatchPattern":"while (!signal.aborted) {\n  try {\n    for await (const event of client.openSnapshotStream({ signal })) {\n      applyEvent(event);\n    }\n  } catch (err) {\n    if (signal.aborted) break;\n    logger.warn(\"snapshot stream ended; reconnecting\", { error: err });\n  }\n  await Bun.sleep(Math.min(baseMs * 2 ** attempt++, maxMs));\n}","preventionTips":["Always wrap the openSnapshotStream loop in a supervisor/reconnect wrapper — the client never auto-reconnects.","Raise proxy/LB idle timeouts above your SSE keepalive interval.","Configure the broker to emit periodic keepalive comment frames to hold connections through NATs.","Monitor broker uptime and stream duration metrics to catch crash loops early."],"tags":["sse","connection-drop","reconnect","auth-broker","streaming"],"backgroundTag":"sse-stream-disconnected","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}