diff --git a/cmd/agent-compose/cli_daemon.go b/cmd/agent-compose/cli_daemon.go index 98bbae55..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,7 +272,15 @@ 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. + // 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, 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, + } if listener.Addr().Network() == "unix" { server.ConnContext = func(ctx context.Context, conn net.Conn) context.Context { if isTrustedUnixSocketConn(conn) { @@ -286,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 f22c50fb..0ef83b93 100644 --- a/cmd/agent-compose/cli_daemon_server_test.go +++ b/cmd/agent-compose/cli_daemon_server_test.go @@ -22,6 +22,29 @@ 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 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) + } +} + 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)