package httpapi import ( "context" "encoding/json" "log/slog" "net/http" "time" "drive.local/drivev2/internal/application" "drive.local/drivev2/internal/config" ) var ErrServerClosed = http.ErrServerClosed type Server struct { httpServer *http.Server } type SystemService interface { Readiness(context.Context) error SetupStatus(context.Context) (application.SetupStatus, error) } func NewServer(cfg config.Config, logger *slog.Logger, system SystemService, frontend http.Handler) *Server { mux := http.NewServeMux() mux.HandleFunc("GET /health/live", jsonResponse(http.StatusOK, map[string]string{"status": "live"})) mux.HandleFunc("GET /health/ready", readinessHandler(logger, system)) mux.HandleFunc("GET /api/v1/setup/status", setupStatusHandler(logger, system)) mux.Handle("/", frontend) return &Server{httpServer: &http.Server{ Addr: cfg.HTTPAddress, Handler: requestLogger(logger, mux), ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 2 * time.Minute, }} } func readinessHandler(logger *slog.Logger, system SystemService) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := system.Readiness(r.Context()); err != nil { logger.Error("readiness check failed", "error", err) problemResponse(w, http.StatusServiceUnavailable, "dependency_unavailable", "A required dependency is unavailable") return } writeJSON(w, http.StatusOK, map[string]string{"status": "ready"}) } } func setupStatusHandler(logger *slog.Logger, system SystemService) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { status, err := system.SetupStatus(r.Context()) if err != nil { logger.Error("setup status failed", "error", err) problemResponse(w, http.StatusServiceUnavailable, "dependency_unavailable", "A required dependency is unavailable") return } writeJSON(w, http.StatusOK, status) } } func (s *Server) ListenAndServe() error { return s.httpServer.ListenAndServe() } func (s *Server) Shutdown(ctx context.Context) error { return s.httpServer.Shutdown(ctx) } func jsonResponse(status int, payload any) http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, status, payload) } } func writeJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(payload); err != nil { slog.Error("encode response", "error", err) } } func problemResponse(w http.ResponseWriter, status int, code, detail string) { w.Header().Set("Content-Type", "application/problem+json") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(map[string]any{ "type": "about:blank", "title": http.StatusText(status), "status": status, "detail": detail, "code": code, }); err != nil { slog.Error("encode problem response", "error", err) } } func requestLogger(logger *slog.Logger, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { started := time.Now() next.ServeHTTP(w, r) logger.Info("http request", "method", r.Method, "path", r.URL.Path, "duration_ms", time.Since(started).Milliseconds()) }) }