// 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" "io/fs" "net" "os" "strings" "sync" "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 } type NetType int const ( NetTCP NetType = iota NetTCP4 NetTCP6 NetUnix NetUnixPacket ) func (n NetType) String() string { switch n { case NetTCP: return "tcp" case NetTCP4: return "tcp4" case NetTCP6: return "tcp6" case NetUnix: return "unix" case NetUnixPacket: return "unixpacket" default: return fmt.Sprintf("NetType(%d)", n) } } func (n *NetType) Set(s string) error { switch strings.ToLower(s) { case "tcp": *n = NetTCP case "tcp4": *n = NetTCP4 case "tcp6": *n = NetTCP6 case "unix": *n = NetUnix case "unixpacket": *n = NetUnixPacket default: return fmt.Errorf("invalid NetType %q", s) } return nil } func (n *NetType) UnmarshalText(b []byte) error { return n.Set(string(b)) } func (n NetType) MarshalText() ([]byte, error) { return n.AppendText(nil) } func (n NetType) AppendText(dst []byte) ([]byte, error) { return append(dst, n.String()...), 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 NetType Address string Mode fs.FileMode } // 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) for _, cfg := range config { g.Go(func() error { ln, err := listenConfig(ctx, cfg) if err != nil { return 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 listenConfig(ctx context.Context, cfg ServerConfig) (net.Listener, error) { network := cfg.Network var lc net.ListenConfig ln, err := lc.Listen(ctx, network.String(), cfg.Address) if err != nil { return nil, fmt.Errorf("failed to listen on %s %s: %w", network, cfg.Address, err) } if _, ok := ln.(*net.UnixListener); cfg.Mode != 0 && ok { if err := os.Chmod(cfg.Address, cfg.Mode); err != nil { ln.Close() return nil, fmt.Errorf("chmod failed on %s: %w", cfg.Address, err) } } return ln, nil } func OpenListeners(ctx context.Context, config []ServerConfig) (Listeners, error) { lns, err := getSystemdListeners() if err != nil { return nil, fmt.Errorf("systemd socket activation failed: %w", err) } if lns != nil { // Systemd is in charge; we don't honor ServerConfig here return lns, nil } return OpenConfigListners(ctx, config) }