package httpserver import ( "context" "encoding/json" "errors" "net" "net/http" "time" "git.toki-labs.com/toki/ariadne/internal/platform/config" "go.uber.org/fx" "go.uber.org/zap" ) type BuildInfo struct { Version string } type Readiness interface { Check(context.Context) error } type Server struct { httpServer *http.Server readiness Readiness logger *zap.Logger buildInfo BuildInfo shutdownTimeout time.Duration } func New( cfg config.Config, readiness Readiness, logger *zap.Logger, buildInfo BuildInfo, ) *Server { server := &Server{ readiness: readiness, logger: logger, buildInfo: buildInfo, shutdownTimeout: cfg.ShutdownTimeout, } server.httpServer = &http.Server{ Addr: cfg.HTTPAddress, Handler: server.routes(), ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 60 * time.Second, } return server } func Register(lifecycle fx.Lifecycle, server *Server) { lifecycle.Append(fx.Hook{ OnStart: server.start, OnStop: server.stop, }) } func (server *Server) start(context.Context) error { listener, err := net.Listen("tcp", server.httpServer.Addr) if err != nil { return err } server.logger.Info("HTTP server listening", zap.String("address", listener.Addr().String())) go func() { err := server.httpServer.Serve(listener) if err != nil && !errors.Is(err, http.ErrServerClosed) { server.logger.Error("HTTP server stopped unexpectedly", zap.Error(err)) } }() return nil } func (server *Server) stop(context.Context) error { ctx, cancel := context.WithTimeout(context.Background(), server.shutdownTimeout) defer cancel() return server.httpServer.Shutdown(ctx) } func (server *Server) routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /healthz", server.health) mux.HandleFunc("GET /readyz", server.ready) return securityHeaders(mux) } func (server *Server) health(writer http.ResponseWriter, _ *http.Request) { writeJSON(writer, http.StatusOK, map[string]any{ "service": "ariadne", "status": "ok", "version": server.buildInfo.Version, }) } func (server *Server) ready(writer http.ResponseWriter, request *http.Request) { ctx, cancel := context.WithTimeout(request.Context(), 2*time.Second) defer cancel() if err := server.readiness.Check(ctx); err != nil { writeJSON(writer, http.StatusServiceUnavailable, map[string]any{ "service": "ariadne", "status": "not_ready", "checks": map[string]string{ "database": "not_ready", }, }) return } writeJSON(writer, http.StatusOK, map[string]any{ "service": "ariadne", "status": "ready", "checks": map[string]string{ "database": "ready", }, }) } func securityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") writer.Header().Set("X-Content-Type-Options", "nosniff") writer.Header().Set("X-Frame-Options", "DENY") next.ServeHTTP(writer, request) }) } func writeJSON(writer http.ResponseWriter, status int, body any) { writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(status) _ = json.NewEncoder(writer).Encode(body) }