package httpserver import ( "context" "errors" "fmt" "net/http" "time" "github.com/go-chi/chi/v5" "code.nochebuena.dev/go/launcher" ) // Logger is the minimal interface httpserver needs — satisfied by logz.Logger. type Logger interface { Info(msg string, args ...any) Error(msg string, err error, args ...any) } // Config holds the HTTP server configuration. type Config struct { Host string `env:"SERVER_HOST" envDefault:"0.0.0.0"` Port int `env:"SERVER_PORT" envDefault:"8080"` ReadTimeout time.Duration `env:"SERVER_READ_TIMEOUT" envDefault:"5s"` WriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT" envDefault:"10s"` IdleTimeout time.Duration `env:"SERVER_IDLE_TIMEOUT" envDefault:"120s"` } // serverOpts holds functional options. type serverOpts struct { middleware []func(http.Handler) http.Handler } // Option configures an httpserver component. type Option func(*serverOpts) // WithMiddleware applies one or more middleware to the root chi router during OnInit. func WithMiddleware(mw ...func(http.Handler) http.Handler) Option { return func(o *serverOpts) { o.middleware = append(o.middleware, mw...) } } // HttpServerComponent is a launcher.Component that also exposes a chi.Router. // Embedding chi.Router gives callers the full routing API: Get, Post, Route, Mount, Use, etc. type HttpServerComponent interface { launcher.Component chi.Router } type httpServer struct { chi.Router logger Logger cfg Config opts serverOpts srv *http.Server } // New creates an HttpServerComponent. No middleware is installed by default; // use [WithMiddleware] to compose the middleware stack. func New(logger Logger, cfg Config, opts ...Option) HttpServerComponent { o := serverOpts{} for _, opt := range opts { opt(&o) } return &httpServer{ Router: chi.NewRouter(), logger: logger, cfg: cfg, opts: o, } } // OnInit applies registered middleware to the router. No-op if none provided. func (s *httpServer) OnInit() error { for _, mw := range s.opts.middleware { s.Router.Use(mw) } return nil } // OnStart starts the HTTP server in a background goroutine. func (s *httpServer) OnStart() error { host := s.cfg.Host if host == "" { host = "0.0.0.0" } port := s.cfg.Port if port == 0 { port = 8080 } s.srv = &http.Server{ Addr: fmt.Sprintf("%s:%d", host, port), Handler: s.Router, ReadTimeout: s.cfg.ReadTimeout, WriteTimeout: s.cfg.WriteTimeout, IdleTimeout: s.cfg.IdleTimeout, } s.logger.Info("httpserver: starting", "addr", s.srv.Addr) go func() { if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { s.logger.Error("httpserver: fatal error", err) } }() return nil } // OnStop performs a graceful shutdown, waiting up to 10 seconds for in-flight // requests to complete. func (s *httpServer) OnStop() error { s.logger.Info("httpserver: shutting down gracefully") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return s.srv.Shutdown(ctx) }