// Package server implements the main HTTP server for the neoirc application. package server import ( "context" "errors" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "go.uber.org/fx" "sneak.berlin/go/neoirc/internal/config" "sneak.berlin/go/neoirc/internal/globals" "sneak.berlin/go/neoirc/internal/handlers" "sneak.berlin/go/neoirc/internal/logger" "sneak.berlin/go/neoirc/internal/middleware" "github.com/getsentry/sentry-go" "github.com/go-chi/chi/v5" _ "github.com/joho/godotenv/autoload" // loads .env file ) const ( shutdownTimeout = 5 * time.Second sentryFlushTime = 2 * time.Second ) // Params defines the dependencies for creating a Server. type Params struct { fx.In Logger *logger.Logger Globals *globals.Globals Config *config.Config Middleware *middleware.Middleware Handlers *handlers.Handlers } // Server is the main HTTP server. // It manages routing, middleware, and lifecycle. type Server struct { startupTime time.Time sentryEnabled bool log *slog.Logger httpServer *http.Server router *chi.Mux params Params mw *middleware.Middleware handlers *handlers.Handlers } // New creates a new Server and registers its lifecycle hooks. func New( lifecycle fx.Lifecycle, params Params, ) (*Server, error) { srv := &Server{ //nolint:exhaustruct // fields set during lifecycle params: params, mw: params.Middleware, handlers: params.Handlers, log: params.Logger.Get(), } lifecycle.Append(fx.Hook{ OnStart: func(_ context.Context) error { srv.startupTime = time.Now() // Build the router before OnStart returns, so srv can // handle requests as soon as the app has started. srv.configure() srv.enableSentry() srv.SetupRoutes() // A closed stdout or stderr must not end the process. signal.Ignore(syscall.SIGPIPE) srv.startHTTP() return nil }, OnStop: func(ctx context.Context) error { srv.cleanShutdown(ctx) return nil }, }) return srv, nil } // ServeHTTP delegates to the chi router. func (srv *Server) ServeHTTP( writer http.ResponseWriter, request *http.Request, ) { srv.router.ServeHTTP(writer, request) } // MaintenanceMode reports whether the server is in maintenance mode. func (srv *Server) MaintenanceMode() bool { return srv.params.Config.MaintenanceMode } func (srv *Server) enableSentry() { srv.sentryEnabled = false if srv.params.Config.SentryDSN == "" { return } err := sentry.Init(sentry.ClientOptions{ //nolint:exhaustruct // only essential fields Dsn: srv.params.Config.SentryDSN, Release: fmt.Sprintf( "%s-%s", srv.params.Globals.Appname, srv.params.Globals.Version, ), }) if err != nil { srv.log.Error("sentry init failure", "error", err) os.Exit(1) } srv.log.Info("sentry error reporting activated") srv.sentryEnabled = true } func (srv *Server) cleanupForExit() { srv.log.Info("cleaning up") } // cleanShutdown stops the HTTP server, waiting up to // shutdownTimeout for the requests in progress to finish. func (srv *Server) cleanShutdown(ctx context.Context) { ctxShutdown, shutdownCancel := context.WithTimeout( ctx, shutdownTimeout, ) err := srv.httpServer.Shutdown(ctxShutdown) if err != nil { srv.log.Error( "server clean shutdown failed", "error", err, ) } if shutdownCancel != nil { shutdownCancel() } srv.cleanupForExit() if srv.sentryEnabled { sentry.Flush(sentryFlushTime) } } func (srv *Server) configure() { // Server configuration placeholder. } // startHTTP creates the HTTP server and serves it from a new // goroutine. srv.httpServer is set before that goroutine // starts and never changes, so it and cleanShutdown read it // without a lock. func (srv *Server) startHTTP() { listenAddr := fmt.Sprintf( ":%d", srv.params.Config.Port, ) srv.httpServer = &http.Server{ //nolint:exhaustruct // optional fields Addr: listenAddr, ReadTimeout: httpReadTimeout, WriteTimeout: httpWriteTimeout, MaxHeaderBytes: maxHeaderBytes, Handler: srv, } go srv.serveUntilShutdown() } func (srv *Server) serveUntilShutdown() { srv.log.Info( "http begin listen", "listenaddr", srv.httpServer.Addr, ) err := srv.httpServer.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { srv.log.Error("listen error", "error", err) } }