From 85e2702cccda272171b7e8608735aa0fe38943bd Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sun, 1 Mar 2026 04:37:26 +0900 Subject: [PATCH] fix: eliminate dual HTTP server bind on gateway port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit channelManager and healthServer both created independent http.Server instances on the same address, causing "address already in use" on startup. Consolidate to a single listener owned by healthServer — channelManager now registers webhook/health handlers directly onto the healthServer mux instead of creating its own server. Also raise healthServer timeouts from 5s to 30s to accommodate webhook request handling. Co-Authored-By: Claude Opus 4.6 --- pkg/channels/manager.go | 56 +++++++---------------------------------- pkg/health/server.go | 4 +-- 2 files changed, 11 insertions(+), 49 deletions(-) diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 155e50b39..2999404e5 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -11,7 +11,6 @@ import ( "errors" "fmt" "math" - "net/http" "sync" "time" @@ -80,8 +79,6 @@ type Manager struct { config *config.Config mediaStore media.MediaStore dispatchTask *asyncTask - mux *http.ServeMux - httpServer *http.Server mu sync.RWMutex placeholders sync.Map // "channel:chatID" → placeholderID (string) typingStops sync.Map // "channel:chatID" → func() @@ -270,41 +267,32 @@ func (m *Manager) initChannels() error { return nil } -// SetupHTTPServer creates a shared HTTP server with the given listen address. -// It registers health endpoints from the health server and discovers channels -// that implement WebhookHandler and/or HealthChecker to register their handlers. -func (m *Manager) SetupHTTPServer(addr string, healthServer *health.Server) { - m.mux = http.NewServeMux() - - // Register health endpoints - if healthServer != nil { - healthServer.RegisterOnMux(m.mux) +// SetupHTTPServer registers channel webhook handlers and health checkers +// onto the provided health server's mux. The health server owns the HTTP +// listener; the channel manager no longer creates its own http.Server. +func (m *Manager) SetupHTTPServer(_ string, healthServer *health.Server) { + if healthServer == nil { + return } + mux := healthServer.Mux() // Discover and register webhook handlers and health checkers for name, ch := range m.channels { if wh, ok := ch.(WebhookHandler); ok { - m.mux.Handle(wh.WebhookPath(), wh) + mux.Handle(wh.WebhookPath(), wh) logger.InfoCF("channels", "Webhook handler registered", map[string]any{ "channel": name, "path": wh.WebhookPath(), }) } if hc, ok := ch.(HealthChecker); ok { - m.mux.HandleFunc(hc.HealthPath(), hc.HealthHandler) + mux.HandleFunc(hc.HealthPath(), hc.HealthHandler) logger.InfoCF("channels", "Health endpoint registered", map[string]any{ "channel": name, "path": hc.HealthPath(), }) } } - - m.httpServer = &http.Server{ - Addr: addr, - Handler: m.mux, - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - } } func (m *Manager) StartAll(ctx context.Context) error { @@ -346,20 +334,6 @@ func (m *Manager) StartAll(ctx context.Context) error { // Start the TTL janitor that cleans up stale typing/placeholder entries go m.runTTLJanitor(dispatchCtx) - // Start shared HTTP server if configured - if m.httpServer != nil { - go func() { - logger.InfoCF("channels", "Shared HTTP server listening", map[string]any{ - "addr": m.httpServer.Addr, - }) - if err := m.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.ErrorCF("channels", "Shared HTTP server error", map[string]any{ - "error": err.Error(), - }) - } - }() - } - logger.InfoC("channels", "All channels started") return nil } @@ -370,18 +344,6 @@ func (m *Manager) StopAll(ctx context.Context) error { logger.InfoC("channels", "Stopping all channels") - // Shutdown shared HTTP server first - if m.httpServer != nil { - shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - if err := m.httpServer.Shutdown(shutdownCtx); err != nil { - logger.ErrorCF("channels", "Shared HTTP server shutdown error", map[string]any{ - "error": err.Error(), - }) - } - m.httpServer = nil - } - // Cancel dispatcher if m.dispatchTask != nil { m.dispatchTask.cancel() diff --git a/pkg/health/server.go b/pkg/health/server.go index 34f2dba3b..17b976084 100644 --- a/pkg/health/server.go +++ b/pkg/health/server.go @@ -48,8 +48,8 @@ func NewServer(host string, port int) *Server { s.server = &http.Server{ Addr: addr, Handler: mux, - ReadTimeout: 5 * time.Second, - WriteTimeout: 5 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, } return s