Skip to content

Commit 3eeeb85

Browse files
committed
fix: change SSE keepalive from event to comment format to unblock client watchdog
Change keepalive ping from SSE event (`event: ping\ndata: ...`) to SSE comment (`: keepalive\n\n`) across all streaming handlers. SSE comments keep HTTP connections alive but are not yielded by the Anthropic SDK, so client-side idle watchdogs (e.g. Claude Code's 90s timeout) continue counting down correctly when the upstream connection dies silently.
1 parent e6e73b4 commit 3eeeb85

4 files changed

Lines changed: 17 additions & 13 deletions

File tree

backend/internal/handler/gateway_helper.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,10 @@ const (
131131
type SSEPingFormat string
132132

133133
const (
134-
// SSEPingFormatClaude is the Claude/Anthropic SSE ping format
135-
SSEPingFormatClaude SSEPingFormat = "data: {\"type\": \"ping\"}\n\n"
134+
// SSEPingFormatClaude uses SSE comment format: keeps HTTP connection alive without
135+
// being yielded by Anthropic SDK, so client idle watchdogs work correctly.
136+
// See streaming-keepalive-design.md
137+
SSEPingFormatClaude SSEPingFormat = ": keepalive\n\n"
136138
// SSEPingFormatNone indicates no ping should be sent (e.g., OpenAI has no ping spec)
137139
SSEPingFormatNone SSEPingFormat = ""
138140
// SSEPingFormatComment is an SSE comment ping for OpenAI/Codex CLI clients

backend/internal/service/antigravity_gateway_service.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4053,9 +4053,9 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context
40534053
if time.Since(lastDataAt) < keepaliveInterval {
40544054
continue
40554055
}
4056-
// SSE ping 事件:Anthropic 原生格式,客户端会正确处理,
4057-
// 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开
4058-
if !cw.Fprintf("event: ping\ndata: {\"type\": \"ping\"}\n\n") {
4056+
// SSE 注释格式 keepalive:在 HTTP 层保持连接活跃,不干扰客户端 idle watchdog。
4057+
// 详见 streaming-keepalive-design.md
4058+
if !cw.Fprintf(": keepalive\n\n") {
40594059
logger.LegacyPrintf("service.antigravity_gateway", "Client disconnected during keepalive ping (antigravity claude), continuing to drain upstream for billing")
40604060
continue
40614061
}
@@ -4457,9 +4457,9 @@ func (s *AntigravityGatewayService) streamUpstreamResponse(c *gin.Context, resp
44574457
if time.Since(lastDataAt) < keepaliveInterval {
44584458
continue
44594459
}
4460-
// SSE ping 事件:Anthropic 原生格式,客户端会正确处理,
4461-
// 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开
4462-
if !cw.Fprintf("event: ping\ndata: {\"type\": \"ping\"}\n\n") {
4460+
// SSE 注释格式 keepalive:在 HTTP 层保持连接活跃,不干扰客户端 idle watchdog。
4461+
// 详见 streaming-keepalive-design.md
4462+
if !cw.Fprintf(": keepalive\n\n") {
44634463
logger.LegacyPrintf("service.antigravity_gateway", "Client disconnected during keepalive ping (antigravity upstream), continuing to drain upstream for billing")
44644464
continue
44654465
}

backend/internal/service/gateway_service.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6939,9 +6939,10 @@ func (s *GatewayService) handleStreamingResponse(ctx context.Context, resp *http
69396939
if time.Since(lastDataAt) < keepaliveInterval {
69406940
continue
69416941
}
6942-
// SSE ping 事件:Anthropic 原生格式,客户端会正确处理,
6943-
// 同时保持连接活跃防止 Cloudflare Tunnel 等代理断开
6944-
if _, werr := fmt.Fprint(w, "event: ping\ndata: {\"type\": \"ping\"}\n\n"); werr != nil {
6942+
// SSE 注释格式 keepalive:在 HTTP 层保持连接活跃(防止 Cloudflare Tunnel 等代理因空闲断开),
6943+
// 但不会被 Anthropic SDK yield 给消费者,不干扰客户端(如 Claude Code)的 idle watchdog。
6944+
// 详见 streaming-keepalive-design.md
6945+
if _, werr := fmt.Fprint(w, ": keepalive\n\n"); werr != nil {
69456946
clientDisconnected = true
69466947
logger.LegacyPrintf("service.gateway", "Client disconnected during keepalive ping, continuing to drain upstream for billing")
69476948
continue

backend/internal/service/openai_gateway_messages.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,8 +552,9 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse(
552552
if time.Since(lastDataAt) < keepaliveInterval {
553553
continue
554554
}
555-
// Send Anthropic-format ping event
556-
if _, err := fmt.Fprint(c.Writer, "event: ping\ndata: {\"type\":\"ping\"}\n\n"); err != nil {
555+
// SSE 注释格式 keepalive:在 HTTP 层保持连接活跃,不干扰客户端 idle watchdog。
556+
// 详见 streaming-keepalive-design.md
557+
if _, err := fmt.Fprint(c.Writer, ": keepalive\n\n"); err != nil {
557558
// Client disconnected
558559
logger.L().Info("openai messages stream: client disconnected during keepalive",
559560
zap.String("request_id", requestID),

0 commit comments

Comments
 (0)