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:
@@ -409,6 +409,69 @@ func TestHTTPTransportSSEStream(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTransportSSEKeepalive(t *testing.T) {
|
||||
transport, ts := testHTTPTransport(t, HTTPConfig{
|
||||
SSEKeepAlive: 50 * time.Millisecond, // Short interval for testing
|
||||
})
|
||||
|
||||
session, _ := transport.sessions.Create()
|
||||
|
||||
// Start SSE stream
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/mcp", nil)
|
||||
req.Header.Set("Mcp-Session-Id", session.ID)
|
||||
req.Header.Set("Accept", "text/event-stream")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read with timeout - should receive keepalive within 100ms
|
||||
buf := make([]byte, 256)
|
||||
done := make(chan struct{})
|
||||
var readData string
|
||||
var readErr error
|
||||
|
||||
go func() {
|
||||
n, err := resp.Body.Read(buf)
|
||||
readData = string(buf[:n])
|
||||
readErr = err
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
if readErr != nil && readErr.Error() != "EOF" {
|
||||
t.Fatalf("Read error: %v", readErr)
|
||||
}
|
||||
// Should receive SSE comment keepalive
|
||||
if !strings.Contains(readData, ":keepalive") {
|
||||
t.Errorf("Expected keepalive comment, got: %q", readData)
|
||||
}
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
t.Error("Timeout waiting for keepalive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTransportSSEKeepaliveDisabled(t *testing.T) {
|
||||
server := NewServer(nil, log.New(io.Discard, "", 0))
|
||||
config := HTTPConfig{
|
||||
SSEKeepAlive: -1, // Explicitly disabled
|
||||
}
|
||||
transport := NewHTTPTransport(server, config)
|
||||
defer transport.sessions.Stop()
|
||||
|
||||
// When SSEKeepAlive is negative, it should remain negative (disabled)
|
||||
if transport.config.SSEKeepAlive != -1 {
|
||||
t.Errorf("Expected SSEKeepAlive to remain -1 (disabled), got %v", transport.config.SSEKeepAlive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTransportParseError(t *testing.T) {
|
||||
_, ts := testHTTPTransport(t, HTTPConfig{})
|
||||
|
||||
@@ -510,6 +573,9 @@ func TestHTTPTransportDefaultConfig(t *testing.T) {
|
||||
if transport.config.ReadHeaderTimeout != DefaultReadHeaderTimeout {
|
||||
t.Errorf("Expected default read header timeout %v, got %v", DefaultReadHeaderTimeout, transport.config.ReadHeaderTimeout)
|
||||
}
|
||||
if transport.config.SSEKeepAlive != DefaultSSEKeepAlive {
|
||||
t.Errorf("Expected default SSE keepalive %v, got %v", DefaultSSEKeepAlive, transport.config.SSEKeepAlive)
|
||||
}
|
||||
|
||||
transport.sessions.Stop()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user