feat: add SSE keepalive messages for connection health
Add configurable SSEKeepAlive interval (default: 15s) that sends SSE comment lines (`:keepalive`) to maintain connection health. Benefits: - Keeps connections alive through proxies/load balancers that timeout idle connections - Detects stale connections earlier (write failures terminate the handler) - Standard SSE pattern - comments are ignored by compliant clients Configuration: - SSEKeepAlive > 0: send keepalives at specified interval - SSEKeepAlive = 0: use default (15s) - SSEKeepAlive < 0: disable keepalives Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ type HTTPConfig struct {
|
||||
WriteTimeout time.Duration // HTTP server write timeout (default: 30s)
|
||||
IdleTimeout time.Duration // HTTP server idle timeout (default: 120s)
|
||||
ReadHeaderTimeout time.Duration // HTTP server read header timeout (default: 10s)
|
||||
SSEKeepAlive time.Duration // SSE keepalive interval (default: 15s, 0 to disable)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -36,6 +37,11 @@ const (
|
||||
DefaultWriteTimeout = 30 * time.Second
|
||||
DefaultIdleTimeout = 120 * time.Second
|
||||
DefaultReadHeaderTimeout = 10 * time.Second
|
||||
|
||||
// DefaultSSEKeepAlive is the default interval for SSE keepalive messages.
|
||||
// These are sent as SSE comments to keep the connection alive through
|
||||
// proxies and load balancers, and to detect stale connections.
|
||||
DefaultSSEKeepAlive = 15 * time.Second
|
||||
)
|
||||
|
||||
// HTTPTransport implements the MCP Streamable HTTP transport.
|
||||
@@ -74,6 +80,10 @@ func NewHTTPTransport(server *Server, config HTTPConfig) *HTTPTransport {
|
||||
if config.ReadHeaderTimeout == 0 {
|
||||
config.ReadHeaderTimeout = DefaultReadHeaderTimeout
|
||||
}
|
||||
// SSEKeepAlive: 0 means use default, negative means disabled
|
||||
if config.SSEKeepAlive == 0 {
|
||||
config.SSEKeepAlive = DefaultSSEKeepAlive
|
||||
}
|
||||
|
||||
return &HTTPTransport{
|
||||
server: server,
|
||||
@@ -302,12 +312,33 @@ func (t *HTTPTransport) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
// Use ResponseController to manage write deadlines for long-lived SSE connections
|
||||
rc := http.NewResponseController(w)
|
||||
|
||||
// Set up keepalive ticker if enabled
|
||||
var keepaliveTicker *time.Ticker
|
||||
var keepaliveChan <-chan time.Time
|
||||
if t.config.SSEKeepAlive > 0 {
|
||||
keepaliveTicker = time.NewTicker(t.config.SSEKeepAlive)
|
||||
keepaliveChan = keepaliveTicker.C
|
||||
defer keepaliveTicker.Stop()
|
||||
}
|
||||
|
||||
// Stream notifications
|
||||
ctx := r.Context()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-keepaliveChan:
|
||||
// Send SSE comment as keepalive (ignored by clients)
|
||||
if err := rc.SetWriteDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
||||
t.server.logger.Printf("Failed to set write deadline: %v", err)
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, ":keepalive\n\n"); err != nil {
|
||||
// Write failed, connection likely closed
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
|
||||
case notification, ok := <-session.Notifications():
|
||||
if !ok {
|
||||
// Session closed
|
||||
@@ -326,7 +357,10 @@ func (t *HTTPTransport) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Write SSE event
|
||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil {
|
||||
// Write failed, connection likely closed
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
|
||||
// Touch session to keep it alive
|
||||
|
||||
Reference in New Issue
Block a user