From cabe41dbca7b94c380564d6dfd65478a9ea39778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=87=91=E5=9D=A4?= Date: Wed, 2 Sep 2026 11:00:07 +0800 Subject: [PATCH 1/3] fix: bound daemon HTTP connections --- cmd/agent-compose/cli_daemon.go | 7 ++++++- cmd/agent-compose/cli_daemon_server_test.go | 16 ++++++++++++++++ test/e2e/graceful_sandbox_stop_contract_test.go | 5 +++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/cmd/agent-compose/cli_daemon.go b/cmd/agent-compose/cli_daemon.go index 98bbae55..d20ce3e7 100644 --- a/cmd/agent-compose/cli_daemon.go +++ b/cmd/agent-compose/cli_daemon.go @@ -271,7 +271,12 @@ func isLoopbackListenAddress(address string) bool { } func (s *daemonServers) add(name, value string, listener net.Listener, handler http.Handler, cleanup func() error) { - server := &http.Server{Handler: h2c.NewHandler(handler, &http2.Server{})} //nolint:staticcheck // h2c is required for unencrypted HTTP/2 compatibility with Connect bidi streams. + server := &http.Server{ + Handler: h2c.NewHandler(handler, &http2.Server{IdleTimeout: 2 * time.Minute, ReadIdleTimeout: 30 * time.Second, PingTimeout: 15 * time.Second, WriteByteTimeout: 30 * time.Second}), //nolint:staticcheck // h2c is required for unencrypted HTTP/2 compatibility with Connect bidi streams. + ReadHeaderTimeout: 5 * time.Second, + IdleTimeout: 2 * time.Minute, + MaxHeaderBytes: 64 << 10, + } if listener.Addr().Network() == "unix" { server.ConnContext = func(ctx context.Context, conn net.Conn) context.Context { if isTrustedUnixSocketConn(conn) { diff --git a/cmd/agent-compose/cli_daemon_server_test.go b/cmd/agent-compose/cli_daemon_server_test.go index f22c50fb..be411c50 100644 --- a/cmd/agent-compose/cli_daemon_server_test.go +++ b/cmd/agent-compose/cli_daemon_server_test.go @@ -22,6 +22,22 @@ import ( "github.com/samber/do/v2" ) +func TestDaemonServerConfiguresConnectionLimits(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen tcp: %v", err) + } + servers := &daemonServers{} + servers.add("HTTP_LISTEN", listener.Addr().String(), listener, http.NewServeMux(), nil) + server := servers.items[0].server + if server.ReadHeaderTimeout != 5*time.Second || server.IdleTimeout != 2*time.Minute || server.MaxHeaderBytes != 64<<10 { + t.Fatalf("server limits = header %s idle %s max-header %d", server.ReadHeaderTimeout, server.IdleTimeout, server.MaxHeaderBytes) + } + if err := servers.shutdown(context.Background()); err != nil { + t.Fatalf("shutdown server: %v", err) + } +} + func TestDaemonTCPServerAttachAgentRunBidiUsesH2C(t *testing.T) { seen := make(chan string, 1) mux := http.NewServeMux() diff --git a/test/e2e/graceful_sandbox_stop_contract_test.go b/test/e2e/graceful_sandbox_stop_contract_test.go index e799be42..a5aa1e41 100644 --- a/test/e2e/graceful_sandbox_stop_contract_test.go +++ b/test/e2e/graceful_sandbox_stop_contract_test.go @@ -9,6 +9,7 @@ import ( "time" "connectrpc.com/connect" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "github.com/chaitin/agent-compose/pkg/agentcompose/api" @@ -68,9 +69,9 @@ func TestGracefulSandboxStopOutcomesUsePublicConnectContract(t *testing.T) { t.Cleanup(server.Close) client := agentcomposev2connect.NewSandboxServiceClient(server.Client(), server.URL) - request := *test.request + request := proto.Clone(test.request).(*agentcomposev2.StopSandboxRequest) request.SandboxId = sandboxID - response, err := client.StopSandbox(context.Background(), connect.NewRequest(&request)) + response, err := client.StopSandbox(context.Background(), connect.NewRequest(request)) if test.wantCode != 0 { if connect.CodeOf(err) != test.wantCode { t.Fatalf("StopSandbox() error = %v, code = %s, want %s", err, connect.CodeOf(err), test.wantCode) From 79cf45f0f447fbc01426ca2f03eb1240de14c989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=87=91=E5=9D=A4?= Date: Wed, 2 Sep 2026 13:00:02 +0800 Subject: [PATCH 2/3] fix: preserve long-lived daemon streams --- cmd/agent-compose/cli_daemon.go | 2 +- cmd/agent-compose/cli_daemon_server_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/agent-compose/cli_daemon.go b/cmd/agent-compose/cli_daemon.go index d20ce3e7..8c576a00 100644 --- a/cmd/agent-compose/cli_daemon.go +++ b/cmd/agent-compose/cli_daemon.go @@ -272,7 +272,7 @@ func isLoopbackListenAddress(address string) bool { func (s *daemonServers) add(name, value string, listener net.Listener, handler http.Handler, cleanup func() error) { server := &http.Server{ - Handler: h2c.NewHandler(handler, &http2.Server{IdleTimeout: 2 * time.Minute, ReadIdleTimeout: 30 * time.Second, PingTimeout: 15 * time.Second, WriteByteTimeout: 30 * time.Second}), //nolint:staticcheck // h2c is required for unencrypted HTTP/2 compatibility with Connect bidi streams. + Handler: h2c.NewHandler(handler, &http2.Server{}), //nolint:staticcheck // h2c is required for unencrypted HTTP/2 compatibility with Connect bidi streams. ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 2 * time.Minute, MaxHeaderBytes: 64 << 10, diff --git a/cmd/agent-compose/cli_daemon_server_test.go b/cmd/agent-compose/cli_daemon_server_test.go index be411c50..e95a49fb 100644 --- a/cmd/agent-compose/cli_daemon_server_test.go +++ b/cmd/agent-compose/cli_daemon_server_test.go @@ -33,6 +33,9 @@ func TestDaemonServerConfiguresConnectionLimits(t *testing.T) { if server.ReadHeaderTimeout != 5*time.Second || server.IdleTimeout != 2*time.Minute || server.MaxHeaderBytes != 64<<10 { t.Fatalf("server limits = header %s idle %s max-header %d", server.ReadHeaderTimeout, server.IdleTimeout, server.MaxHeaderBytes) } + if server.WriteTimeout != 0 { + t.Fatalf("server WriteTimeout = %s, want zero for long-running streams", server.WriteTimeout) + } if err := servers.shutdown(context.Background()); err != nil { t.Fatalf("shutdown server: %v", err) } From c85e248d186775d684f960618c743a4e6bbf7ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=87=91=E5=9D=A4?= Date: Wed, 2 Sep 2026 13:06:05 +0800 Subject: [PATCH 3/3] test: lock in long-lived h2c settings --- cmd/agent-compose/cli_daemon.go | 7 ++++++- cmd/agent-compose/cli_daemon_server_test.go | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/agent-compose/cli_daemon.go b/cmd/agent-compose/cli_daemon.go index 8c576a00..a6779c0b 100644 --- a/cmd/agent-compose/cli_daemon.go +++ b/cmd/agent-compose/cli_daemon.go @@ -66,6 +66,7 @@ type daemonServer struct { listener net.Listener server *http.Server cleanup func() error + h2c *http2.Server } type localUnixSocketRequestKey struct{} @@ -271,8 +272,11 @@ func isLoopbackListenAddress(address string) bool { } func (s *daemonServers) add(name, value string, listener net.Listener, handler http.Handler, cleanup func() error) { + // Keep HTTP/2 connection timeouts disabled: this server carries long-lived + // Connect bidi streams, whose idle periods are part of the protocol. + h2cConfig := &http2.Server{} server := &http.Server{ - Handler: h2c.NewHandler(handler, &http2.Server{}), //nolint:staticcheck // h2c is required for unencrypted HTTP/2 compatibility with Connect bidi streams. + Handler: h2c.NewHandler(handler, h2cConfig), //nolint:staticcheck // h2c is required for unencrypted HTTP/2 compatibility with Connect bidi streams. ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 2 * time.Minute, MaxHeaderBytes: 64 << 10, @@ -291,6 +295,7 @@ func (s *daemonServers) add(name, value string, listener net.Listener, handler h listener: listener, server: server, cleanup: cleanup, + h2c: h2cConfig, }) } diff --git a/cmd/agent-compose/cli_daemon_server_test.go b/cmd/agent-compose/cli_daemon_server_test.go index e95a49fb..0ef83b93 100644 --- a/cmd/agent-compose/cli_daemon_server_test.go +++ b/cmd/agent-compose/cli_daemon_server_test.go @@ -36,6 +36,10 @@ func TestDaemonServerConfiguresConnectionLimits(t *testing.T) { if server.WriteTimeout != 0 { t.Fatalf("server WriteTimeout = %s, want zero for long-running streams", server.WriteTimeout) } + config := servers.items[0].h2c + if config == nil || config.IdleTimeout != 0 || config.ReadIdleTimeout != 0 || config.PingTimeout != 0 || config.WriteByteTimeout != 0 { + t.Fatalf("h2c connection limits = %#v, want all zero for long-running streams", config) + } if err := servers.shutdown(context.Background()); err != nil { t.Fatalf("shutdown server: %v", err) }