Add production-ready HTTP server package with routing, health checks, and middleware

Introduces server/ sub-package as the server-side companion to the existing Client.
Includes Router (over http.ServeMux with groups and mounting), graceful shutdown with
signal handling, health endpoints (/healthz, /readyz), and built-in middlewares
(RequestID, Recovery, Logging). Zero external dependencies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 13:41:54 +03:00
parent 6b901c931e
commit cea75d198b
12 changed files with 1215 additions and 0 deletions

173
server/server.go Normal file
View File

@@ -0,0 +1,173 @@
package server
import (
"context"
"errors"
"log/slog"
"net"
"net/http"
"os/signal"
"sync/atomic"
"syscall"
"time"
)
// Server is a production-ready HTTP server with graceful shutdown,
// middleware support, and signal handling.
type Server struct {
httpServer *http.Server
listener net.Listener
addr atomic.Value
logger *slog.Logger
shutdownTimeout time.Duration
onShutdown []func()
listenAddr string
}
// New creates a new Server that will serve the given handler with the
// provided options. Middleware from options is applied to the handler.
func New(handler http.Handler, opts ...Option) *Server {
o := &serverOptions{
addr: ":8080",
shutdownTimeout: 15 * time.Second,
}
for _, opt := range opts {
opt(o)
}
// Apply middleware chain to the handler.
if len(o.middlewares) > 0 {
handler = Chain(o.middlewares...)(handler)
}
srv := &Server{
httpServer: &http.Server{
Handler: handler,
ReadTimeout: o.readTimeout,
ReadHeaderTimeout: o.readHeaderTimeout,
WriteTimeout: o.writeTimeout,
IdleTimeout: o.idleTimeout,
},
logger: o.logger,
shutdownTimeout: o.shutdownTimeout,
onShutdown: o.onShutdown,
listenAddr: o.addr,
}
return srv
}
// ListenAndServe starts the server and blocks until a SIGINT or SIGTERM
// signal is received. It then performs a graceful shutdown within the
// configured shutdown timeout.
//
// Returns nil on clean shutdown or an error if listen/shutdown fails.
func (s *Server) ListenAndServe() error {
ln, err := net.Listen("tcp", s.listenAddr)
if err != nil {
return err
}
s.listener = ln
s.addr.Store(ln.Addr().String())
s.log("server started", slog.String("addr", ln.Addr().String()))
// Wait for signal in context.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
errCh := make(chan error, 1)
go func() {
if err := s.httpServer.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
close(errCh)
}()
select {
case err := <-errCh:
return err
case <-ctx.Done():
stop()
return s.shutdown()
}
}
// ListenAndServeTLS starts the server with TLS and blocks until a signal
// is received.
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
ln, err := net.Listen("tcp", s.listenAddr)
if err != nil {
return err
}
s.listener = ln
s.addr.Store(ln.Addr().String())
s.log("server started (TLS)", slog.String("addr", ln.Addr().String()))
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
errCh := make(chan error, 1)
go func() {
if err := s.httpServer.ServeTLS(ln, certFile, keyFile); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
close(errCh)
}()
select {
case err := <-errCh:
return err
case <-ctx.Done():
stop()
return s.shutdown()
}
}
// Shutdown gracefully shuts down the server. It calls any registered
// onShutdown hooks, then waits for active connections to drain within
// the shutdown timeout.
func (s *Server) Shutdown(ctx context.Context) error {
s.runOnShutdown()
return s.httpServer.Shutdown(ctx)
}
// Addr returns the listener address after the server has started.
// Returns an empty string if the server has not started yet.
func (s *Server) Addr() string {
v := s.addr.Load()
if v == nil {
return ""
}
return v.(string)
}
func (s *Server) shutdown() error {
s.log("shutting down")
s.runOnShutdown()
ctx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout)
defer cancel()
if err := s.httpServer.Shutdown(ctx); err != nil {
s.log("shutdown error", slog.String("error", err.Error()))
return err
}
s.log("server stopped")
return nil
}
func (s *Server) runOnShutdown() {
for _, fn := range s.onShutdown {
fn()
}
}
func (s *Server) log(msg string, attrs ...slog.Attr) {
if s.logger != nil {
s.logger.LogAttrs(context.Background(), slog.LevelInfo, msg, attrs...)
}
}