Small Go module for running HTTP servers with graceful shutdown and sensible defaults.
Requires Go 1.27 or newer.
go get github.com/containeroo/httpgraceRun(ctx, listenAddr, handler, logger, opts...)builds anhttp.Serverand runs it.RunServer(ctx, server, logger)runs an already configured*http.Server.SignalContext(parent, signals...)creates a signal-aware context with cancellation causes.RunandRunServerreturn startup and shutdown errors to the caller.loggeris optional; whennil, lifecycle logging is disabled.
Run accepts focused functional options for overriding individual defaults:
WithReadHeaderTimeoutWithWriteTimeoutWithIdleTimeoutWithMaxHeaderValueCountWithShutdownTimeout
WithOptions remains available when the complete option set should be replaced.
ReadHeaderTimeout:10sWriteTimeout:15sIdleTimeout:60sMaxHeaderValueCount:http.DefaultMaxHeaderValueCountShutdownTimeout:10s
Go 1.27 defines http.DefaultMaxHeaderValueCount as 500.
SignalContext creates a context that is canceled when a configured signal is received.
The received signal is stored as the cancellation cause, and shutdown logs include
context.Cause(ctx).
By default, SignalContext listens for:
os.InterruptSIGTERMon Unix platforms
To treat additional signals, such as SIGHUP, as shutdown signals, pass them explicitly:
ctx, stop := server.SignalContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
syscall.SIGHUP,
)
defer stop()package main
import (
"context"
"log/slog"
"net/http"
"os"
"github.com/containeroo/httpgrace/server"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok"))
})
ctx, stop := server.SignalContext(context.Background())
defer stop()
if err := server.Run(ctx, ":8080", mux, logger); err != nil {
logger.Error("server stopped with error", "err", err)
}
}Use the focused functional options when only individual defaults should change:
if err := server.Run(
ctx,
":8080",
mux,
logger,
server.WithMaxHeaderValueCount(100),
server.WithShutdownTimeout(15*time.Second),
); err != nil {
logger.Error("server stopped with error", "err", err)
}Settings that are not explicitly overridden retain their httpgrace defaults.
Use WithOptions when the complete configuration should be supplied by the caller:
if err := server.Run(
ctx,
":8080",
mux,
logger,
server.WithOptions(server.Options{
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
MaxHeaderValueCount: 100,
ShutdownTimeout: 15 * time.Second,
}),
); err != nil {
logger.Error("server stopped with error", "err", err)
}Use RunServer when the caller wants full control over http.Server:
package main
import (
"context"
"log/slog"
"net/http"
"os"
"time"
"github.com/containeroo/httpgrace/server"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
MaxHeaderValueCount: 100,
}
ctx, stop := server.SignalContext(context.Background())
defer stop()
if err := server.RunServer(ctx, srv, logger); err != nil {
logger.Error("server stopped with error", "err", err)
}
}This module was inspired by the Grafana blog post:
https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.