this repo has no description
at main 143 lines 3.1 kB view raw
1package main 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "log/slog" 8 "net/http" 9 "os" 10 "os/signal" 11 "syscall" 12 "time" 13 14 "github.com/labstack/echo/v4" 15 "github.com/labstack/echo/v4/middleware" 16 slogecho "github.com/samber/slog-echo" 17) 18 19type Server struct { 20 echo *echo.Echo 21 httpd *http.Server 22 logger *slog.Logger 23} 24 25type Config struct { 26 Logger *slog.Logger 27 Bind string 28} 29 30func NewServer(config Config) (*Server, error) { 31 logger := config.Logger 32 if logger == nil { 33 logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 34 Level: slog.LevelInfo, 35 })) 36 } 37 38 e := echo.New() 39 40 // httpd 41 var ( 42 httpTimeout = 1 * time.Minute 43 httpMaxHeaderBytes = 1 * (1024 * 1024) 44 ) 45 46 srv := &Server{ 47 echo: e, 48 logger: logger, 49 } 50 51 srv.httpd = &http.Server{ 52 Handler: srv, 53 Addr: config.Bind, 54 WriteTimeout: httpTimeout, 55 ReadTimeout: httpTimeout, 56 MaxHeaderBytes: httpMaxHeaderBytes, 57 } 58 59 e.HideBanner = true 60 e.Use(slogecho.New(logger)) 61 e.Use(middleware.Recover()) 62 e.Use(middleware.BodyLimit("4M")) 63 e.HTTPErrorHandler = srv.errorHandler 64 e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ 65 ContentTypeNosniff: "nosniff", 66 XFrameOptions: "SAMEORIGIN", 67 HSTSMaxAge: 31536000, // 365 days 68 // TODO: 69 // ContentSecurityPolicy 70 // XSSProtection 71 })) 72 73 e.GET("/", srv.WebHome) 74 e.GET("/_health", srv.HandleHealthCheck) 75 76 // XXX 77 return srv, nil 78} 79 80func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 81 srv.echo.ServeHTTP(rw, req) 82} 83 84func (srv *Server) Run() error { 85 srv.logger.Info("starting server", "bind", srv.httpd.Addr) 86 go func() { 87 if err := srv.httpd.ListenAndServe(); err != nil { 88 if !errors.Is(err, http.ErrServerClosed) { 89 srv.logger.Error("HTTP server shutting down unexpectedly", "err", err) 90 } 91 } 92 }() 93 94 // Wait for a signal to exit. 95 srv.logger.Info("registering OS exit signal handler") 96 quit := make(chan struct{}) 97 exitSignals := make(chan os.Signal, 1) 98 signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) 99 go func() { 100 sig := <-exitSignals 101 srv.logger.Info("received OS exit signal", "signal", sig) 102 103 // Shut down the HTTP server 104 if err := srv.Shutdown(); err != nil { 105 srv.logger.Error("HTTP server shutdown error", "err", err) 106 } 107 108 // Trigger the return that causes an exit. 109 close(quit) 110 }() 111 <-quit 112 srv.logger.Info("graceful shutdown complete") 113 return nil 114} 115 116func (srv *Server) Shutdown() error { 117 srv.logger.Info("shutting down") 118 119 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 120 defer cancel() 121 122 return srv.httpd.Shutdown(ctx) 123} 124 125type GenericError struct { 126 Error string `json:"error"` 127 Message string `json:"message"` 128} 129 130func (srv *Server) errorHandler(err error, c echo.Context) { 131 code := http.StatusInternalServerError 132 var errorMessage string 133 if he, ok := err.(*echo.HTTPError); ok { 134 code = he.Code 135 errorMessage = fmt.Sprintf("%s", he.Message) 136 } 137 if code >= 500 { 138 srv.logger.Warn("atbin-http-internal-error", "err", err) 139 } 140 if !c.Response().Committed { 141 c.JSON(code, GenericError{Error: "InternalError", Message: errorMessage}) 142 } 143}