error-pages/internal/http/server.go
2024-07-03 18:12:13 +04:00

148 lines
3.9 KiB
Go

package http
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/valyala/fasthttp"
"gh.tarampamp.am/error-pages/internal/appmeta"
"gh.tarampamp.am/error-pages/internal/config"
ep "gh.tarampamp.am/error-pages/internal/http/handlers/error_page"
"gh.tarampamp.am/error-pages/internal/http/handlers/live"
"gh.tarampamp.am/error-pages/internal/http/handlers/static"
"gh.tarampamp.am/error-pages/internal/http/handlers/version"
"gh.tarampamp.am/error-pages/internal/http/middleware/logreq"
"gh.tarampamp.am/error-pages/internal/logger"
)
// Server is an HTTP server for serving error pages.
type Server struct {
log *logger.Logger
server *fasthttp.Server
beforeStop func()
}
// NewServer creates a new HTTP server.
func NewServer(log *logger.Logger, readBufferSize uint) Server {
const (
readTimeout = 30 * time.Second
writeTimeout = readTimeout + 10*time.Second // should be bigger than the read timeout
)
return Server{
log: log,
server: &fasthttp.Server{
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
ReadBufferSize: int(readBufferSize),
DisablePreParseMultipartForm: true,
NoDefaultServerHeader: true,
CloseOnShutdown: true,
Logger: logger.NewStdLog(log),
},
beforeStop: func() {}, // noop
}
}
// Register server handlers, middlewares, etc.
func (s *Server) Register(cfg *config.Config) error { //nolint:funlen
var (
liveHandler = live.New()
versionHandler = version.New(appmeta.Version())
faviconHandler = static.New(static.Favicon)
errorPagesHandler, closeCache = ep.New(cfg, s.log)
notFound = http.StatusText(http.StatusNotFound) + "\n"
notAllowed = http.StatusText(http.StatusMethodNotAllowed) + "\n"
)
// wrap the before shutdown function to close the cache
s.beforeStop = closeCache
s.server.Handler = func(ctx *fasthttp.RequestCtx) {
var url, method = string(ctx.Path()), string(ctx.Method())
switch {
// live endpoints
case url == "/healthz" || url == "/health/live" || url == "/health" || url == "/live":
liveHandler(ctx)
// version endpoint
case url == "/version":
versionHandler(ctx)
// favicon.ico endpoint
case url == "/favicon.ico":
faviconHandler(ctx)
// error pages endpoints:
// - /
// - /{code}.html
// - /{code}.htm
// - /{code}
//
// the HTTP method is not limited to GET and HEAD - it can be any
case url == "/" || ep.URLContainsCode(url) || ep.HeadersContainCode(&ctx.Request.Header):
errorPagesHandler(ctx)
// wrong requests handling
default:
switch {
case method == fasthttp.MethodHead:
ctx.Error(notAllowed, fasthttp.StatusNotFound)
case method == fasthttp.MethodGet:
ctx.Error(notFound, fasthttp.StatusNotFound)
default:
ctx.Error(notAllowed, fasthttp.StatusMethodNotAllowed)
}
}
}
// apply middleware
s.server.Handler = logreq.New(s.log, func(ctx *fasthttp.RequestCtx) bool {
// skip logging healthcheck and .ico (favicon) requests
return strings.Contains(strings.ToLower(string(ctx.UserAgent())), "healthcheck") ||
strings.HasSuffix(string(ctx.Path()), ".ico")
})(s.server.Handler)
return nil
}
// Start server.
func (s *Server) Start(ip string, port uint16) (err error) {
if net.ParseIP(ip) == nil {
return errors.New("invalid IP address")
}
var ln net.Listener
if strings.Count(ip, ":") >= 2 { //nolint:mnd // ipv6
if ln, err = net.Listen("tcp6", fmt.Sprintf("[%s]:%d", ip, port)); err != nil {
return err
}
} else { // ipv4
if ln, err = net.Listen("tcp4", fmt.Sprintf("%s:%d", ip, port)); err != nil {
return err
}
}
return s.server.Serve(ln)
}
// Stop server gracefully.
func (s *Server) Stop(timeout time.Duration) error {
var ctx, cancel = context.WithTimeout(context.Background(), timeout)
defer cancel()
s.beforeStop()
return s.server.ShutdownWithContext(ctx)
}