diff --git a/internal/di/providers.go b/internal/di/providers.go index 77b8e29..853f3ad 100644 --- a/internal/di/providers.go +++ b/internal/di/providers.go @@ -145,7 +145,7 @@ func ProvideErrorBus() fx.Option { func ProvideHealthRegistry() fx.Option { return fx.Provide(func(dbClient *database.Client) (*health.Registry, error) { registry := health.NewRegistry() - + // Register database health checker registry.Register("database", health.NewDatabaseChecker(dbClient)) @@ -237,19 +237,54 @@ func ProvideHTTPServer() fx.Option { host = "0.0.0.0" } addr := fmt.Sprintf("%s:%d", host, port) - + + log.Info("HTTP server starting", + logger.String("addr", addr), + ) + // Start server in a goroutine + // ListenAndServe blocks, so we need to start it async + // If there's an immediate error (like port in use), it will return quickly + errChan := make(chan error, 1) go func() { - log.Info("HTTP server starting", - logger.String("addr", addr), - ) if err := srv.Start(); err != nil && err != http.ErrServerClosed { - log.Error("HTTP server error", + log.Error("HTTP server failed", logger.String("error", err.Error()), ) + select { + case errChan <- err: + default: + } } }() - return nil + + // Wait a short time to detect immediate binding errors + // If ListenAndServe fails immediately (e.g., port in use), it will return quickly + select { + case err := <-errChan: + return fmt.Errorf("HTTP server failed to start: %w", err) + case <-time.After(500 * time.Millisecond): + // If no error after 500ms, verify server is actually listening + // by attempting a connection + client := &http.Client{Timeout: 1 * time.Second} + checkURL := fmt.Sprintf("http://localhost:%d/healthz", port) + resp, err := client.Get(checkURL) + if err != nil { + // Server might still be starting, but log the attempt + log.Warn("Could not verify HTTP server is listening (may still be starting)", + logger.String("url", checkURL), + logger.String("error", err.Error()), + ) + // Continue anyway - server might still be starting + } else { + resp.Body.Close() + } + + log.Info("HTTP server started successfully", + logger.String("addr", addr), + ) + return nil + } }, OnStop: func(ctx context.Context) error { return srv.Shutdown(ctx) diff --git a/internal/server/server.go b/internal/server/server.go index 4584df7..47a4883 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -123,9 +123,23 @@ func registerRoutes( // Start starts the HTTP server. func (s *Server) Start() error { + // ListenAndServe will block until the server is closed + // If there's an immediate error (like port in use), it will return immediately return s.httpServer.ListenAndServe() } +// StartAsync starts the HTTP server in a goroutine and returns a channel that signals when it's ready or errored. +func (s *Server) StartAsync() <-chan error { + errChan := make(chan error, 1) + go func() { + if err := s.Start(); err != nil && err != http.ErrServerClosed { + errChan <- err + } + close(errChan) + }() + return errChan +} + // Shutdown gracefully shuts down the HTTP server. func (s *Server) Shutdown(ctx context.Context) error { return s.httpServer.Shutdown(ctx)