Documentation
¶
Overview ¶
Package httpserver provides a configurable, production-oriented HTTP server bootstrap for Go services.
Problem ¶
Starting a robust HTTP service usually requires repetitive infrastructure code: listener setup, route registration, middleware chaining, panic/404/405 handling, request timeouts, TLS wiring, graceful shutdown, and optional observability endpoints. Implementing this independently in each service leads to inconsistency and duplicated maintenance effort.
Solution ¶
This package defines a reusable server assembly with:
- option-driven configuration (New + functional [Option]s)
- pluggable route binding via Binder
- configurable default route set for operational endpoints
- shared middleware application model
- graceful shutdown orchestration via context and/or signal channel
Custom service routes are supplied by a Binder, while default routes can be enabled selectively (or all at once) through options.
Default Operational Routes ¶
When enabled, the built-in route set includes:
- /ip: returns the service public IP (via ipify integration)
- /metrics: returns metrics payload (501 by default unless replaced)
- /ping: lightweight liveness endpoint
- /pprof/*option: pprof profiling endpoints
- /status: service health endpoint
- / (index): generated route index
Features ¶
- Graceful lifecycle control: non-blocking start, context-aware shutdown, configurable shutdown timeout, external wait-group and signal integration, abnormal-termination reporting via HTTPServer.ServeError, and ephemeral port discovery via HTTPServer.Addr.
- Startup validation: misconfigured routes and options (nil handlers or middleware, duplicate or malformed routes, unknown default route identifiers) are reported by New as wrapped sentinel errors instead of panics.
- Router defaults: standardized not-found, method-not-allowed, and panic handlers with structured logging.
- Middleware pipeline: common middleware (logger/timeout) plus global and per-route middleware composition, with per-route timeout override or opt-out (DisableTimeout).
- Observability integration: trace-id propagation hooks, HTTP data redaction, per-request log entries carrying the response status code and size, optional pprof/metrics/status routes, and net/http internal diagnostics routed to the structured logger.
- Transport flexibility: plain TCP or TLS (HTTP/1.1 and HTTP/2 via ALPN) from cert/key material (WithTLSCertData) or a fully custom WithTLSConfig.
- Safe defaults with extensibility: overridable handlers and server parameters for production customization.
Security ¶
The default operational routes expose service internals: /pprof/*option serves runtime profiles (memory layout, goroutine stacks, CPU traces), the index route enumerates every registered endpoint, /metrics may reveal implementation details, and /ip performs an outbound call to a third-party service. Enable these routes only on internal or administrative listeners that are not reachable from the public internet, or protect them with authentication middleware appropriate for your environment.
Benefits ¶
httpserver reduces service bootstrap boilerplate, enforces consistent runtime behavior across projects, and accelerates delivery of HTTP services with operational best practices built in.
For a usage example, refer to examples/service/internal/cli/bind.go.
Index ¶
- Constants
- Variables
- func ApplyMiddleware(arg MiddlewareArgs, next http.Handler, middleware ...MiddlewareFn) http.Handler
- func LoggerMiddlewareFn(args MiddlewareArgs, next http.Handler) http.Handler
- func RequestInjectHandler(logger *slog.Logger, traceIDHeaderName string, redactFn RedactFn, ...) http.Handler
- type Binder
- type DefaultRoute
- type GetPublicIPFunc
- type HTTPServer
- type Index
- type IndexHandlerFunc
- type MiddlewareArgs
- type MiddlewareFn
- type Option
- func WithEnableAllDefaultRoutes() Option
- func WithEnableDefaultRoutes(ids ...DefaultRoute) Option
- func WithIPHandlerFunc(handler http.HandlerFunc) Option
- func WithIndexHandlerFunc(handler IndexHandlerFunc) Option
- func WithLogger(logger *slog.Logger) Option
- func WithMaxRequestBodyBytes(n int64) Option
- func WithMethodNotAllowedHandlerFunc(handler http.HandlerFunc) Option
- func WithMetricsHandlerFunc(handler http.HandlerFunc) Option
- func WithMiddlewareFn(fn ...MiddlewareFn) Option
- func WithNotFoundHandlerFunc(handler http.HandlerFunc) Option
- func WithPProfHandlerFunc(handler http.HandlerFunc) Option
- func WithPanicHandlerFunc(handler http.HandlerFunc) Option
- func WithPingHandlerFunc(handler http.HandlerFunc) Option
- func WithRedactFn(fn RedactFn) Option
- func WithRequestTimeout(timeout time.Duration) Option
- func WithRouter(r *httprouter.Router) Option
- func WithServerAddr(addr string) Option
- func WithServerIdleTimeout(timeout time.Duration) Option
- func WithServerMaxHeaderBytes(n int) Option
- func WithServerReadHeaderTimeout(timeout time.Duration) Option
- func WithServerReadTimeout(timeout time.Duration) Option
- func WithServerWriteTimeout(timeout time.Duration) Option
- func WithShutdownSignalChan(ch chan struct{}) Option
- func WithShutdownTimeout(timeout time.Duration) Option
- func WithShutdownWaitGroup(wg *sync.WaitGroup) Option
- func WithStatusHandlerFunc(handler http.HandlerFunc) Option
- func WithTLSCertData(pemCert, pemKey []byte) Option
- func WithTLSConfig(tlsConfig *tls.Config) Option
- func WithTraceIDHeaderName(name string) Option
- func WithoutDefaultRouteLogger(routes ...DefaultRoute) Option
- func WithoutMethodNotAllowedLogger() Option
- func WithoutNotFoundLogger() Option
- func WithoutRouteLogger() Option
- type RedactFn
- type Route
Examples ¶
Constants ¶
const DisableTimeout time.Duration = -1
DisableTimeout is a sentinel Route.Timeout value that disables the request timeout for a single route, overriding any global timeout set with WithRequestTimeout. Use it for streaming or long-running endpoints (e.g. pprof profiles or server-sent events).
Variables ¶
var ( // ErrNilBinder is returned by New when a nil Binder is supplied. ErrNilBinder = errors.New("binder is required") // ErrNilRouteHandler is returned when a route is declared without a handler. ErrNilRouteHandler = errors.New("route handler is required") // ErrNilRouteMiddleware is returned when a route declares a nil middleware function. ErrNilRouteMiddleware = errors.New("route middleware function is required") // ErrInvalidRouteMethod is returned when a route has an empty HTTP method. ErrInvalidRouteMethod = errors.New("invalid route method") // ErrInvalidRoutePath is returned when a route path is empty or does not start with '/'. ErrInvalidRoutePath = errors.New("invalid route path") // ErrDuplicateRoute is returned when two routes share the same method and path. ErrDuplicateRoute = errors.New("duplicate route") // ErrRouteRegistration is returned when the underlying router rejects a route // (e.g. conflicting wildcard or malformed pattern). ErrRouteRegistration = errors.New("route registration failed") // ErrUnknownDefaultRoute is returned when an unknown default route identifier is enabled. ErrUnknownDefaultRoute = errors.New("unknown default route") // ErrInvalidTLSConfig is returned when a TLS configuration carries no // certificate material (no Certificates, GetCertificate, or GetConfigForClient). ErrInvalidTLSConfig = errors.New("invalid TLS configuration: no Certificates, GetCertificate, or GetConfigForClient set") )
Functions ¶
func ApplyMiddleware ¶
func ApplyMiddleware(arg MiddlewareArgs, next http.Handler, middleware ...MiddlewareFn) http.Handler
ApplyMiddleware returns an http Handler with all middleware handler functions applied. Nil middleware entries are skipped, so the function is safe to call with partially populated middleware lists.
func LoggerMiddlewareFn ¶
func LoggerMiddlewareFn(args MiddlewareArgs, next http.Handler) http.Handler
LoggerMiddlewareFn returns the middleware handler function to handle logs.
func RequestInjectHandler ¶
func RequestInjectHandler( logger *slog.Logger, traceIDHeaderName string, redactFn RedactFn, rnd *random.Rnd, next http.Handler, ) http.Handler
RequestInjectHandler wraps all incoming requests and injects a logger in the request scoped context.
Nil arguments fall back to safe defaults (slog.Default(), a new random generator, and the redact.HTTPDataString redact function), so the returned handler never panics on missing dependencies.
The final log entry includes the response status code (response_code, with the implicit 200 recorded for handlers that never call WriteHeader) and the number of body bytes written (response_size). Hijacked connections (e.g. WebSocket upgrades) never write an HTTP status through the writer and are therefore logged with the implicit 200 as well.
The writer passed to next forwards http.Flusher, http.Hijacker, http.Pusher, io.ReaderFrom, and http.ResponseController (via Unwrap), but not the deprecated http.CloseNotifier; use Request.Context() for cancelation instead.
At debug level the whole request (headers and body) is dumped into the log entry via httputil.DumpRequest, which buffers the entire body in memory; keep this in mind when enabling debug logging for endpoints that accept large bodies.
Types ¶
type DefaultRoute ¶
type DefaultRoute string
DefaultRoute is the type for the default route names.
const ( // IndexRoute is the identifier to enable the index handler. IndexRoute DefaultRoute = "index" // IPRoute is the identifier to enable the ip handler. IPRoute DefaultRoute = "ip" // MetricsRoute is the identifier to enable the metrics handler. MetricsRoute DefaultRoute = "metrics" // PingRoute is the identifier to enable the ping handler. PingRoute DefaultRoute = "ping" // PprofRoute is the identifier to enable the pprof handler. PprofRoute DefaultRoute = "pprof" // StatusRoute is the identifier to enable the status handler. StatusRoute DefaultRoute = "status" )
type GetPublicIPFunc ¶
GetPublicIPFunc resolves the service public IP address.
func GetPublicIPDefaultFunc ¶
func GetPublicIPDefaultFunc() GetPublicIPFunc
GetPublicIPDefaultFunc returns the GetPublicIP function for a default ipify client.
type HTTPServer ¶
type HTTPServer struct {
// contains filtered or unexported fields
}
HTTPServer defines the HTTP Server object.
func New ¶
New constructs HTTP server with router, middleware, default operational routes, TLS, and graceful shutdown orchestration.
The listener is bound immediately: on success the server already holds the network address. If the returned server is never started, call HTTPServer.Shutdown to release the bound listener. A nil ctx is treated as context.Background().
Note: the underlying httprouter may emit trailing-slash and fixed-path redirects (HTTP 301) and automatic OPTIONS responses before the middleware pipeline runs; those are not passed through the request logger. Supply a custom router via WithRouter to change that behavior.
Example ¶
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"time"
"github.com/tecnickcom/nurago/pkg/httpserver"
)
// exampleBinder supplies the custom service routes.
type exampleBinder struct{}
func (b *exampleBinder) BindHTTP(_ context.Context) []httpserver.Route {
return []httpserver.Route{
{
Method: http.MethodGet,
Path: "/hello",
Description: "Says hello.",
Handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("hello"))
},
},
}
}
func main() {
ctx := context.Background()
srv, err := httpserver.New(
ctx,
&exampleBinder{},
httpserver.WithServerAddr(":0"), // ephemeral port; see srv.Addr() for the actual address
httpserver.WithEnableDefaultRoutes(httpserver.PingRoute, httpserver.StatusRoute),
httpserver.WithRequestTimeout(30*time.Second),
httpserver.WithShutdownTimeout(5*time.Second),
httpserver.WithLogger(slog.New(slog.DiscardHandler)),
)
if err != nil {
fmt.Println(err)
return
}
// The server runs in the background; canceling ctx or calling Shutdown
// stops it gracefully.
srv.StartServer()
// ... serve traffic ...
err = srv.Shutdown(ctx)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("server stopped")
}
Output: server stopped
func (*HTTPServer) Addr ¶
func (h *HTTPServer) Addr() net.Addr
Addr returns the network address the server listener is bound to. It is primarily useful when binding to an ephemeral port (":0") to discover the actual port assigned by the operating system.
func (*HTTPServer) ServeError ¶
func (h *HTTPServer) ServeError() <-chan error
ServeError returns a channel that receives the error that caused the server to stop serving unexpectedly (any Serve failure other than a normal shutdown). The channel is buffered (size 1) and never receives a nil value. A server that stops through Shutdown leaves it empty.
func (*HTTPServer) Shutdown ¶
func (h *HTTPServer) Shutdown(ctx context.Context) error
Shutdown gracefully shuts down server with timeout enforcement; wraps net/http Server.Shutdown(). It is safe to call Shutdown any number of times and from multiple paths (e.g. manually and via the internal context/signal goroutine): the wait group is decremented exactly once, and only when the server was actually started.
func (*HTTPServer) StartServer ¶
func (h *HTTPServer) StartServer()
StartServer starts server in background using context from New().
func (*HTTPServer) StartServerCtx ¶
func (h *HTTPServer) StartServerCtx(ctx context.Context)
StartServerCtx starts server in background goroutine with context-aware shutdown support.
The provided ctx controls shutdown only: its cancelation triggers the graceful-shutdown sequence. Request-scoped context values are inherited from the ctx passed to New, not from this one.
It is a no-op if the server has already been started or has already been shut down: this keeps the shutdown wait group balanced (a single Add(1) matched by a single decrement) and avoids serving twice on the same listener.
type Index ¶
type Index struct {
// Routes is the list of routes attached to the current service.
Routes []Route `json:"routes"`
}
Index contains the list of routes attached to the current service.
type IndexHandlerFunc ¶
type IndexHandlerFunc func([]Route) http.HandlerFunc
IndexHandlerFunc builds an index handler from the registered route list.
type MiddlewareArgs ¶
type MiddlewareArgs struct {
// Method is the HTTP method (e.g.: GET, POST, PUT, DELETE, ...).
Method string
// Path is the URL path.
Path string
// Description is the description of the route or a general description for the handler.
Description string
// TraceIDHeaderName is the Trace ID header name.
TraceIDHeaderName string
// RedactFunc is the function used to redact HTTP request and response dumps in the logs.
RedactFunc RedactFn
// Logger is the logger.
Logger *slog.Logger
// Rnd is the random generator.
Rnd *random.Rnd
}
MiddlewareArgs contains extra optional arguments to be passed to the middleware handler function MiddlewareFn.
type MiddlewareFn ¶
type MiddlewareFn func(args MiddlewareArgs, next http.Handler) http.Handler
MiddlewareFn is a function that wraps an http.Handler.
type Option ¶
type Option func(*config) error
Option configures an HTTPServer instance.
func WithEnableAllDefaultRoutes ¶
func WithEnableAllDefaultRoutes() Option
WithEnableAllDefaultRoutes enables all default routes on the server.
func WithEnableDefaultRoutes ¶
func WithEnableDefaultRoutes(ids ...DefaultRoute) Option
WithEnableDefaultRoutes sets the default routes to be enabled on the server. An unknown route identifier returns ErrUnknownDefaultRoute.
func WithIPHandlerFunc ¶
func WithIPHandlerFunc(handler http.HandlerFunc) Option
WithIPHandlerFunc replaces the default ip handler function.
func WithIndexHandlerFunc ¶
func WithIndexHandlerFunc(handler IndexHandlerFunc) Option
WithIndexHandlerFunc replaces the index handler. The handler receives every bound route except the index route itself. The http.HandlerFunc it returns must not be nil.
func WithLogger ¶
WithLogger overrides the default logger.
func WithMaxRequestBodyBytes ¶
WithMaxRequestBodyBytes sets the maximum number of bytes the server will read from a request body, complementing WithServerMaxHeaderBytes for public-listener hardening. Reads beyond the limit fail with an *http.MaxBytesError (see http.MaxBytesHandler), so handlers can detect it and respond with 413 Request Entity Too Large. When not set, the request body size is unlimited. Per-route limits can be layered on top with Route.Middleware.
func WithMethodNotAllowedHandlerFunc ¶
func WithMethodNotAllowedHandlerFunc(handler http.HandlerFunc) Option
WithMethodNotAllowedHandlerFunc http handler called when a request cannot be routed.
func WithMetricsHandlerFunc ¶
func WithMetricsHandlerFunc(handler http.HandlerFunc) Option
WithMetricsHandlerFunc replaces the default metrics handler function.
func WithMiddlewareFn ¶
func WithMiddlewareFn(fn ...MiddlewareFn) Option
WithMiddlewareFn adds one or more middleware handler functions to all routes (endpoints). These middleware handlers are applied in the provided order after the default ones and before the custom route ones. Nil middleware functions are rejected.
func WithNotFoundHandlerFunc ¶
func WithNotFoundHandlerFunc(handler http.HandlerFunc) Option
WithNotFoundHandlerFunc http handler called when no matching route is found.
func WithPProfHandlerFunc ¶
func WithPProfHandlerFunc(handler http.HandlerFunc) Option
WithPProfHandlerFunc replaces the default pprof handler function.
func WithPanicHandlerFunc ¶
func WithPanicHandlerFunc(handler http.HandlerFunc) Option
WithPanicHandlerFunc http handler to handle panics recovered from http handlers.
func WithPingHandlerFunc ¶
func WithPingHandlerFunc(handler http.HandlerFunc) Option
WithPingHandlerFunc replaces the default ping handler function.
func WithRedactFn ¶
WithRedactFn set the function used to redact HTTP request and response dumps in the logs.
func WithRequestTimeout ¶
WithRequestTimeout sets a time limit for all routes after which a request receives a 503 Service Unavailable. Alternatively a custom timeout handler like http.TimeoutHandler can be added via WithMiddlewareFn().
The timeout is enforced with http.TimeoutHandler, which buffers the whole response in memory and does not support streaming (Flusher/Hijacker); exempt streaming routes with a negative Route.Timeout (see DisableTimeout).
func WithRouter ¶
func WithRouter(r *httprouter.Router) Option
WithRouter replaces the default router used by the httpServer (mostly used for test purposes with a mock router). The supplied router is mutated by New (default handlers are installed and routes are registered incrementally), so it must not be reused after a failed New call.
func WithServerAddr ¶
WithServerAddr sets the address the httpServer will bind to.
func WithServerIdleTimeout ¶
WithServerIdleTimeout sets the maximum amount of time to wait for the next request when keep-alives are enabled. If zero, there is no idle timeout.
func WithServerMaxHeaderBytes ¶
WithServerMaxHeaderBytes sets the maximum number of bytes the server will read parsing a request's headers (including the request line). It does not limit the request body size. When not set, the net/http default (http.DefaultMaxHeaderBytes, 1 MiB) applies. Lowering it is a standard hardening measure for public listeners.
func WithServerReadHeaderTimeout ¶
WithServerReadHeaderTimeout sets the read header timeout.
func WithServerReadTimeout ¶
WithServerReadTimeout sets the read timeout.
func WithServerWriteTimeout ¶
WithServerWriteTimeout sets the write timeout. This is a server-wide connection deadline: long-running or streaming responses (e.g. /pprof/profile?seconds=N) are cut off when it elapses, even for routes exempted from the request timeout with DisableTimeout, so size it to the longest response the server must produce.
func WithShutdownSignalChan ¶
func WithShutdownSignalChan(ch chan struct{}) Option
WithShutdownSignalChan sets the shared channel used to signal a shutdown. When a value is received on the channel the server initiates the shutdown process. Close the channel (rather than sending a single value) to wake every server that shares it, since a single send is delivered to only one receiver.
func WithShutdownTimeout ¶
WithShutdownTimeout sets the shutdown timeout.
func WithShutdownWaitGroup ¶
WithShutdownWaitGroup sets the shared waiting group to communicate externally when the server is shutdown. The counter is incremented when the server starts (StartServer/StartServerCtx) and decremented exactly once on shutdown, so Wait returns immediately if called before the server has been started.
func WithStatusHandlerFunc ¶
func WithStatusHandlerFunc(handler http.HandlerFunc) Option
WithStatusHandlerFunc replaces the default status handler function.
func WithTLSCertData ¶
WithTLSCertData enables TLS with the given certificate and key data. The resulting configuration advertises HTTP/2 and HTTP/1.1 via ALPN, so clients can negotiate either protocol.
func WithTLSConfig ¶
WithTLSConfig sets a custom TLS configuration, giving full control over certificates, client authentication, and cipher suites. When both this and WithTLSCertData are supplied, the last option wins. To serve HTTP/2, include "h2" (and "http/1.1") in NextProtos; WithTLSCertData does this automatically.
func WithTraceIDHeaderName ¶
WithTraceIDHeaderName overrides the default trace id header name.
func WithoutDefaultRouteLogger ¶
func WithoutDefaultRouteLogger(routes ...DefaultRoute) Option
WithoutDefaultRouteLogger disables the logger handler for the specified default routes. An unknown route identifier returns ErrUnknownDefaultRoute.
func WithoutMethodNotAllowedLogger ¶
func WithoutMethodNotAllowedLogger() Option
WithoutMethodNotAllowedLogger disables the request logger for the 405 Method Not Allowed handler.
func WithoutNotFoundLogger ¶
func WithoutNotFoundLogger() Option
WithoutNotFoundLogger disables the request logger for the 404 Not Found handler. This is useful to avoid noisy logs from scanners probing unknown paths.
func WithoutRouteLogger ¶
func WithoutRouteLogger() Option
WithoutRouteLogger disables the logger handler for all routes.
type RedactFn ¶
RedactFn redacts sensitive values from logged byte payloads and returns a redacted string.
type Route ¶
type Route struct {
// Method is the HTTP method (e.g.: GET, POST, PUT, DELETE, ...).
// It must be uppercase: the router matches methods case-sensitively, so a
// lowercase method would never match standard requests.
Method string `json:"method"`
// Path is the URL path.
Path string `json:"path"`
// Description is the description of this route that is displayed by the /index endpoint.
Description string `json:"description"`
// Handler is the handler function.
Handler http.HandlerFunc `json:"-"`
// Middleware is a set of middleware to apply to this route.
Middleware []MiddlewareFn `json:"-"`
// DisableLogger disable the default logger when set to true.
DisableLogger bool `json:"-"`
// Timeout time limit after which a request receives a 503 Service Unavailable.
// A positive value overrides the common value set with WithRequestTimeout.
// A negative value (see DisableTimeout) disables the timeout for this route.
// Zero leaves the common value in effect.
Timeout time.Duration `json:"-"`
}
Route contains the HTTP route description.