All checks were successful
check / check (push) Successful in 2m43s
Replace github.com/go-chi/chi v1.5.5 with github.com/go-chi/chi/v5 v5.2.1 to resolve GO-2026-4316 (open redirect in RedirectSlashes middleware). Update all import paths across the codebase and update documentation references in README.md and CONVENTIONS.md.
220 lines
4.5 KiB
Go
220 lines
4.5 KiB
Go
// 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"
|
|
|
|
"git.eeqj.de/sneak/neoirc/internal/config"
|
|
"git.eeqj.de/sneak/neoirc/internal/globals"
|
|
"git.eeqj.de/sneak/neoirc/internal/handlers"
|
|
"git.eeqj.de/sneak/neoirc/internal/logger"
|
|
"git.eeqj.de/sneak/neoirc/internal/middleware"
|
|
"go.uber.org/fx"
|
|
|
|
"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
|
|
exitCode int
|
|
sentryEnabled bool
|
|
log *slog.Logger
|
|
ctx context.Context //nolint:containedctx // signal handling pattern
|
|
cancelFunc context.CancelFunc
|
|
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()
|
|
go srv.Run() //nolint:contextcheck
|
|
|
|
return nil
|
|
},
|
|
OnStop: func(_ context.Context) error {
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return srv, nil
|
|
}
|
|
|
|
// Run starts the server configuration, Sentry, and begins serving.
|
|
func (srv *Server) Run() {
|
|
srv.configure()
|
|
srv.enableSentry()
|
|
srv.serve()
|
|
}
|
|
|
|
// 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) serve() int {
|
|
srv.ctx, srv.cancelFunc = context.WithCancel(
|
|
context.Background(),
|
|
)
|
|
|
|
go func() {
|
|
sigCh := make(chan os.Signal, 1)
|
|
|
|
signal.Ignore(syscall.SIGPIPE)
|
|
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
|
|
|
sig := <-sigCh
|
|
|
|
srv.log.Info("signal received", "signal", sig)
|
|
|
|
if srv.cancelFunc != nil {
|
|
srv.cancelFunc()
|
|
}
|
|
}()
|
|
|
|
go srv.serveUntilShutdown()
|
|
|
|
<-srv.ctx.Done()
|
|
|
|
srv.cleanShutdown()
|
|
|
|
return srv.exitCode
|
|
}
|
|
|
|
func (srv *Server) cleanupForExit() {
|
|
srv.log.Info("cleaning up")
|
|
}
|
|
|
|
func (srv *Server) cleanShutdown() {
|
|
srv.exitCode = 0
|
|
|
|
ctxShutdown, shutdownCancel := context.WithTimeout(
|
|
context.Background(), 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.
|
|
}
|
|
|
|
func (srv *Server) serveUntilShutdown() {
|
|
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,
|
|
}
|
|
|
|
srv.SetupRoutes()
|
|
|
|
srv.log.Info(
|
|
"http begin listen", "listenaddr", listenAddr,
|
|
)
|
|
|
|
err := srv.httpServer.ListenAndServe()
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
srv.log.Error("listen error", "error", err)
|
|
|
|
if srv.cancelFunc != nil {
|
|
srv.cancelFunc()
|
|
}
|
|
}
|
|
}
|