// Package http provides utilities for managing HTTP servers and listeners. // // It supports opening multiple network listeners from configuration, // running multiple HTTP servers concurrently with graceful shutdown, // and integrates structured logging with context. // // This package is designed to be used with context cancellation for concurrency control. package http import ( "context" "errors" "fmt" "log/slog" "net" "net/http" "sync" "time" "go.sudomsg.com/kit/logging" "golang.org/x/sync/errgroup" ) // Listeners is a slice of net.Listener interfaces representing multiple network listeners. type Listeners []net.Listener // CloseAll closes all listeners in the slice. // It aggregates all errors returned by individual Close calls using errors.Join. // // If no errors occur, it returns nil. func (ls Listeners) CloseAll() error { var errs []error for _, l := range ls { if err := l.Close(); err != nil { errs = append(errs, err) } } if len(errs) > 0 { return errors.Join(errs...) } return nil } // ServerConfig defines a single network listener configuration. // // Network is the network type, e.g., "tcp". // Address is the socket address, e.g., ":8080". type ServerConfig struct { Network string `toml:"network"` Address string `toml:"network"` } // OpenConfigListeners opens network listeners as specified by the provided ServerConfig slice. // // It attempts to open all listeners concurrently and returns them if all succeed. // // If any listener fails to open, it closes all previously opened listeners and returns an error. // // The context controls cancellation of the opening process. func OpenConfigListners(ctx context.Context, config []ServerConfig) (Listeners, error) { lns := make(Listeners, 0, len(config)) var mu sync.Mutex g, ctx := errgroup.WithContext(ctx) var lc net.ListenConfig for _, cfg := range config { g.Go(func() error { network := cfg.Network if network == "" { network = "tcp" } ln, err := lc.Listen(ctx, network, cfg.Address) if err != nil { return fmt.Errorf("failed to listen on %s %s: %w", network, cfg.Address, err) } mu.Lock() lns = append(lns, ln) mu.Unlock() return nil }) } if err := g.Wait(); err != nil { _ = lns.CloseAll() return nil, err } return lns, nil } func OpenListeners(ctx context.Context, config []ServerConfig) (Listeners, error) { return OpenConfigListners(ctx, config) } // RunHTTPServers runs HTTP servers concurrently on all provided listeners. // // The provided handler is used for all servers. // Servers respond to context cancellation by performing a graceful shutdown with a timeout of 10 seconds. // // Logging is performed using the slog.Logger extracted from context. // Each server logs startup, shutdown, and errors. // // This function blocks until all servers have stopped or an error occurs. func RunHTTPServers(ctx context.Context, lns Listeners, handler http.Handler) error { g, ctx := errgroup.WithContext(ctx) for _, ln := range lns { g.Go(func() error { logger, ctx := logging.With(ctx, "address", ln.Addr) srv := &http.Server{ Addr: ln.Addr().String(), Handler: handler, BaseContext: func(l net.Listener) context.Context { return ctx }, ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), } logger.Log(ctx, slog.LevelInfo, "HTTP server serving") if err := httpServeContext(ctx, srv, ln); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("HTTP server Serve Error: %w", err) } return nil }) } return g.Wait() } func httpServeContext(ctx context.Context, srv *http.Server, ln net.Listener) error { logger := logging.FromContext(ctx) go func() { <-ctx.Done() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() err := srv.Shutdown(ctx) if err != nil { logger.Log(ctx, slog.LevelWarn, "HTTP server Shutdown Error", slog.Any("error", err)) } else { logger.Log(ctx, slog.LevelInfo, "HTTP Server Shutdown Complete") } }() return srv.Serve(ln) }