Skip to content

feat(streaming): add SSE keepalive and error handling - #2860

Merged
steebchen merged 2 commits into
mainfrom
claude/socket-connection-gateway-51w3mo
Jun 30, 2026
Merged

steebchen merged 2 commits into
mainfrom
claude/socket-connection-gateway-51w3mo

Conversation

@steebchen

@steebchen steebchen commented Jun 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Improves reliability of Anthropic streaming responses by adding SSE keepalive pings to prevent proxy/load balancer timeouts during quiet gaps, and implements proper error handling for mid-stream failures that gracefully terminates the SSE stream instead of abruptly closing the connection.

Key Changes

  • SSE Keepalive: Added 15-second keepalive interval that emits SSE ping comments (: ping\n) to keep the connection alive during slow time-to-first-token, long reasoning phases, or slow tool argument generation. These pings are consumed by the translator and never reach the client.

  • Improved Streaming Error Handling: Replaced immediate HTTP exception throwing (which cannot work after headers are sent) with proper SSE error event emission:

    • Detects client-side aborts (AbortError) and logs them as info-level events
    • For other errors, emits a well-formed Anthropic error event followed by a message_stop event, allowing clients to end the stream cleanly
    • Includes fallback error handling if SSE event writing itself fails
  • Cleanup: Added clearInterval(keepaliveInterval) in the finally block to ensure the keepalive timer is properly cleaned up when the stream ends.

Implementation Details

The keepalive mechanism uses SSE spec-compliant comment syntax (: ping\n) which is ignored by the Anthropic SDK but serves to keep the connection active. This prevents "The socket connection was closed unexpectedly" errors that clients would otherwise see when the connection is terminated by proxies or load balancers during quiet periods.

The error handling distinguishes between client-initiated aborts (which are expected and logged at info level) and actual streaming errors (which are logged at error level and communicated to the client via SSE events).

https://claude.ai/code/session_018uuGUkp6NaPEVSRw553RYJ

Summary by CodeRabbit

  • Bug Fixes
    • Added periodic SSE : ping keepalive messages during streaming responses to reduce idle timeout/disconnect issues.
    • Improved mid-stream error handling to send clean, terminal SSE sequences when failures occur.
    • Better handles client disconnects/aborts by avoiding unnecessary failure events and suppressing secondary send errors.

Native Anthropic streaming had no keepalive, so idle gaps (slow
first token, long reasoning, slow tool-arg generation) let proxies
and clients time out the socket, surfacing as "The socket connection
was closed unexpectedly" in coding clients. Add a 15s SSE ping,
mirroring the chat-completions path.

Also stop throwing HTTPException from the streaming catch block:
headers are already sent, so it tore down the socket instead of
producing an HTTP error. Emit a well-formed Anthropic error event +
message_stop so the client ends cleanly, and treat client aborts as
info-level.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uuGUkp6NaPEVSRw553RYJ
@github-actions github-actions Bot changed the title Add SSE keepalive and improve streaming error handling feat(streaming): add SSE keepalive and error handling Jun 30, 2026
@steebchen
steebchen enabled auto-merge June 30, 2026 15:37
@coderabbitai

coderabbitai Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bcc71f4a-c6cc-4be4-b298-f6362c9c510f

📥 Commits

Reviewing files that changed from the base of the PR and between e9ba98d and bd5cc0b.

📒 Files selected for processing (1)
  • apps/gateway/src/responses/responses.ts

Walkthrough

The Anthropic and /v1/responses streaming paths now send periodic : ping SSE keepalives. Both handlers also changed mid-stream error handling to treat aborts separately and to emit terminal SSE events instead of relying on thrown HTTP errors.

Changes

SSE streaming updates

Layer / File(s) Summary
Anthropic keepalive and terminal SSE errors
apps/gateway/src/anthropic/anthropic.ts
Adds : ping SSE comments during streaming, clears the interval in finally, and replaces mid-stream HTTP 500 throws with logged Anthropic error and message_stop SSE events; AbortError is handled separately.
/v1/responses keepalive and failed response events
apps/gateway/src/responses/responses.ts
Adds a 15s : ping keepalive timer, clears it in finally, and changes streaming error handling so aborts skip response.failed while other errors log and attempt to emit response.failed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main streaming reliability changes: SSE keepalive pings and improved error handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/socket-connection-gateway-51w3mo

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/gateway/src/anthropic/anthropic.ts (1)

796-801: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider terminating the keepalive comment with a blank line (\n\n).

A lone : ping\n is a valid comment but is not a self-terminating SSE frame; the line stays in the parser's pending buffer until the next event boundary. Emitting : ping\n\n makes the keepalive a complete, immediately-dispatched frame, which is the conventional form and avoids any dependency on a subsequent event to flush it. Bytes still reach the proxy either way, so this is non-blocking.

♻️ Proposed tweak
-				const keepaliveInterval = setInterval(() => {
-					stream.write(": ping\n").catch(() => {
+				const keepaliveInterval = setInterval(() => {
+					stream.write(": ping\n\n").catch(() => {
 						// Stream likely closed; cleanup happens in finally.
 					});
 				}, KEEPALIVE_INTERVAL_MS);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/gateway/src/anthropic/anthropic.ts` around lines 796 - 801, The
keepalive in `anthropic.ts` is sending a comment frame with only `: ping\n`,
which may stay buffered instead of being dispatched immediately. Update the
`setInterval` keepalive write so the SSE comment is terminated as a complete
frame, and keep the change localized to the keepalive logic in the streaming
handler around `KEEPALIVE_INTERVAL_MS` and `stream.write`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/gateway/src/anthropic/anthropic.ts`:
- Around line 796-801: The keepalive in `anthropic.ts` is sending a comment
frame with only `: ping\n`, which may stay buffered instead of being dispatched
immediately. Update the `setInterval` keepalive write so the SSE comment is
terminated as a complete frame, and keep the change localized to the keepalive
logic in the streaming handler around `KEEPALIVE_INTERVAL_MS` and
`stream.write`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9eede167-a9d8-49b2-bd78-89e25028f0c9

📥 Commits

Reviewing files that changed from the base of the PR and between af4707e and e9ba98d.

📒 Files selected for processing (1)
  • apps/gateway/src/anthropic/anthropic.ts

@steebchen
steebchen added this pull request to the merge queue Jun 30, 2026
The Responses API streaming translator had no keepalive, so idle gaps
(slow first token, long reasoning, slow tool-arg generation) let
proxies and clients time out the socket, surfacing as "The socket
connection was closed unexpectedly" in coding clients. Add a 15s SSE
ping, matching the chat-completions and messages paths.

Also harden the streaming catch: skip the terminal write on client
aborts (the socket is gone) and guard the response.failed write so a
secondary failure can't escape the handler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uuGUkp6NaPEVSRw553RYJ
@steebchen
steebchen removed this pull request from the merge queue due to a manual request Jun 30, 2026
@steebchen
steebchen enabled auto-merge June 30, 2026 15:54

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd5cc0b902

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +421 to +425
const keepaliveInterval = setInterval(() => {
stream.write(": ping\n").catch(() => {
// Stream likely closed; cleanup happens in finally.
});
}, KEEPALIVE_INTERVAL_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wrap the first SSE write in the cleanup path

If the client disconnects before or during the initial response.created write just below this block, that await stream.writeSSE(...) rejects before execution enters the try/finally that clears this interval. The new keepalive timer will then keep firing and swallowing failed writes for the lifetime of the process, so early-aborted /v1/responses streams can leak timers; start the interval inside the guarded section or include the initial write in the same try/finally.

Useful? React with 👍 / 👎.

@steebchen
steebchen added this pull request to the merge queue Jun 30, 2026
Merged via the queue into main with commit fca561b Jun 30, 2026
17 checks passed
@steebchen
steebchen deleted the claude/socket-connection-gateway-51w3mo branch June 30, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants