httpserver depends on launcher (Tier 2), placing it at Tier 3. With launcher corrected from Tier 5 to Tier 2, httpserver's tier drops accordingly.
117 lines
3.0 KiB
Go
117 lines
3.0 KiB
Go
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)
|
|
}
|