// Package httpserver composes the Fiber app and its middleware stack. // // The actual route tree is registered by internal/pkg/openapi through the // humafiber adapter, so this package only owns lifecycle: configuration, // graceful start, graceful shutdown, and basic observability middleware. package httpserver import ( "context" "fmt" "log/slog" "net/http" "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/fiber/v2/middleware/requestid" "anchorage/internal/pkg/metrics" ) // Options configures a Server. type Options struct { Host string Port int ReadTimeout time.Duration WriteTimeout time.Duration // MetricsACLCIDRs is the allowlist for /metrics. Nil → use the // metrics package default (loopback + RFC1918). Explicit empty // slice → no restriction (firewall-only). MetricsACLCIDRs []string } // Server wraps *fiber.App with lifecycle helpers. type Server struct { App *fiber.App opts Options } // New constructs a Server with the standard anchorage middleware stack. // // The returned *fiber.App is exposed on Server.App so callers can register // routes before Start is called. func New(opts Options) *Server { app := fiber.New(fiber.Config{ AppName: "anchorage", DisableStartupMessage: true, ReadTimeout: opts.ReadTimeout, WriteTimeout: opts.WriteTimeout, ErrorHandler: func(c *fiber.Ctx, err error) error { // Fiber default returns an HTML body; JSON is more useful // for our API clients. code := fiber.StatusInternalServerError if fe, ok := err.(*fiber.Error); ok { code = fe.Code } return c.Status(code).JSON(fiber.Map{ "error": http.StatusText(code), "message": err.Error(), }) }, }) app.Use(recover.New()) app.Use(requestid.New()) app.Use(accessLog()) // /metrics lives at root (NOT under /v1) so Prometheus scrapers // find it at the conventional path. Gated by a CIDR ACL — see // metrics.ACL and MetricsConfig.AllowCIDRs. if acl, err := metrics.ACL(opts.MetricsACLCIDRs); err != nil { slog.Error("httpserver: bad metrics ACL; /metrics disabled", "err", err) } else { app.Get("/metrics", acl, metrics.Handler()) } return &Server{App: app, opts: opts} } // Start binds and listens. Returns only on listener error. func (s *Server) Start(_ context.Context) error { addr := fmt.Sprintf("%s:%d", s.opts.Host, s.opts.Port) slog.Info("httpserver: listening", "addr", addr) return s.App.Listen(addr) } // Shutdown drains in-flight requests with a bounded grace period. func (s *Server) Shutdown(ctx context.Context) error { return s.App.ShutdownWithContext(ctx) }