package main import ( "context" "errors" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" slogecho "github.com/samber/slog-echo" ) type Server struct { echo *echo.Echo httpd *http.Server logger *slog.Logger } type Config struct { Logger *slog.Logger Bind string } func NewServer(config Config) (*Server, error) { logger := config.Logger if logger == nil { logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, })) } e := echo.New() // httpd var ( httpTimeout = 1 * time.Minute httpMaxHeaderBytes = 1 * (1024 * 1024) ) srv := &Server{ echo: e, logger: logger, } srv.httpd = &http.Server{ Handler: srv, Addr: config.Bind, WriteTimeout: httpTimeout, ReadTimeout: httpTimeout, MaxHeaderBytes: httpMaxHeaderBytes, } e.HideBanner = true e.Use(slogecho.New(logger)) e.Use(middleware.Recover()) e.Use(middleware.BodyLimit("4M")) e.HTTPErrorHandler = srv.errorHandler e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ ContentTypeNosniff: "nosniff", XFrameOptions: "SAMEORIGIN", HSTSMaxAge: 31536000, // 365 days // TODO: // ContentSecurityPolicy // XSSProtection })) e.GET("/", srv.WebHome) e.GET("/_health", srv.HandleHealthCheck) // XXX return srv, nil } func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { srv.echo.ServeHTTP(rw, req) } func (srv *Server) Run() error { srv.logger.Info("starting server", "bind", srv.httpd.Addr) go func() { if err := srv.httpd.ListenAndServe(); err != nil { if !errors.Is(err, http.ErrServerClosed) { srv.logger.Error("HTTP server shutting down unexpectedly", "err", err) } } }() // Wait for a signal to exit. srv.logger.Info("registering OS exit signal handler") quit := make(chan struct{}) exitSignals := make(chan os.Signal, 1) signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) go func() { sig := <-exitSignals srv.logger.Info("received OS exit signal", "signal", sig) // Shut down the HTTP server if err := srv.Shutdown(); err != nil { srv.logger.Error("HTTP server shutdown error", "err", err) } // Trigger the return that causes an exit. close(quit) }() <-quit srv.logger.Info("graceful shutdown complete") return nil } func (srv *Server) Shutdown() error { srv.logger.Info("shutting down") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return srv.httpd.Shutdown(ctx) } type GenericError struct { Error string `json:"error"` Message string `json:"message"` } func (srv *Server) errorHandler(err error, c echo.Context) { code := http.StatusInternalServerError var errorMessage string if he, ok := err.(*echo.HTTPError); ok { code = he.Code errorMessage = fmt.Sprintf("%s", he.Message) } if code >= 500 { srv.logger.Warn("atbin-http-internal-error", "err", err) } if !c.Response().Committed { c.JSON(code, GenericError{Error: "InternalError", Message: errorMessage}) } }