httpserver

package
v0.3.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 22, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Overview

Package httpserver provides a production-ready HTTP server with graceful shutdown, observability, and middleware support.

Quick Start

Create a server with your handler and start it:

mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)

server := httpserver.New(
    httpserver.WithServiceName("my-api"),
    httpserver.WithHandler(mux),
)

if err := server.ListenAndServe(ctx); err != nil {
    log.Fatal(err)
}

ServiceName Integration

The server's ServiceName is automatically propagated to all observability components. Set it once and it appears everywhere:

server := httpserver.New(
    httpserver.WithServiceName("payment-api"),  // Set once
    httpserver.WithTracing(httpserver.TracingConfig{}),    // Gets ServiceName
    httpserver.WithMetrics(httpserver.MetricsConfig{}),    // Gets ServiceName
    httpserver.WithLogging(httpserver.LoggerConfig{...}),  // Gets ServiceName
    httpserver.WithHealth(&health, "1.0.0"),               // Gets ServiceName
    httpserver.WithHandler(mux),
)

Configuration

Use preset configurations as a starting point:

// For production with hardened timeouts
server := httpserver.New(
    httpserver.WithConfig(httpserver.ProductionConfig()),
    httpserver.WithServiceName("payment-api"),
    httpserver.WithHandler(mux),
)

Rate Limiting

Global rate limiting (all requests):

server := httpserver.New(
    httpserver.WithRateLimit(httpserver.RateLimitConfig{
        Limit: 100,  // 100 req/sec
        Burst: 200,
    }),
    httpserver.WithHandler(mux),
)

Per-endpoint rate limiting (apply middleware to specific routes):

// Stricter limit for login
mux.Handle("/api/login", httpserver.RateLimitByIP(10, 20)(loginHandler))

// Different limit for API
mux.Handle("/api/", httpserver.RateLimit(httpserver.RateLimitConfig{
    Limit: 50,
    Burst: 100,
})(apiHandler))

Distributed rate limiting with Redis:

rdb := redis.NewUniversalClient(&redis.UniversalOptions{
    Addrs: []string{"localhost:6379"},
})
mux.Handle("/api/", httpserver.RateLimitByIPRedis(rdb, 100, 200)(apiHandler))

Health Checks

Register health endpoints with auto-configured ServiceName:

var health *httpserver.HealthHandler
server := httpserver.New(
    httpserver.WithServiceName("my-api"),
    httpserver.WithHealth(&health, "1.0.0"),
    httpserver.WithHandler(mux),
)

health.AddReadinessCheck("database", dbPingCheck)
health.AddReadinessCheck("redis", redisPingCheck)

mux.Handle("/ping", health.PingHandler())
mux.Handle("/livez", health.LiveHandler())
mux.Handle("/readyz", health.ReadyHandler())

Framework Adapters

Use adapters for popular frameworks:

import "github.com/kroma-labs/sentinel-go/httpserver/adapters/chi"
import "github.com/kroma-labs/sentinel-go/httpserver/adapters/gin"
import "github.com/kroma-labs/sentinel-go/httpserver/adapters/echo"
import "github.com/kroma-labs/sentinel-go/httpserver/adapters/fiber"
import "github.com/kroma-labs/sentinel-go/httpserver/adapters/grpcgateway"

Index

Constants

View Source
const RequestIDHeader = "X-Request-ID"

RequestIDHeader is the header key for request IDs.

Variables

View Source
var ErrInvalidCredentials = errors.New("invalid credentials")

ErrInvalidCredentials is returned when authentication fails. Intentionally generic to not reveal which part of credentials is wrong.

Functions

func ClientIDFromContext

func ClientIDFromContext(ctx context.Context) string

ClientIDFromContext returns the client_id from the request context. Returns empty string if not authenticated via ServiceAuth.

func PprofHandler

func PprofHandler(cfg PprofConfig) http.Handler

PprofHandler returns an http.Handler that serves pprof endpoints.

Available endpoints:

  • /debug/pprof/ - Index page
  • /debug/pprof/cmdline - Command line
  • /debug/pprof/profile - CPU profile
  • /debug/pprof/symbol - Symbol lookup
  • /debug/pprof/trace - Execution trace
  • /debug/pprof/heap - Heap profile
  • /debug/pprof/goroutine - Goroutine profile
  • /debug/pprof/block - Block profile
  • /debug/pprof/mutex - Mutex profile
  • /debug/pprof/allocs - Allocation profile
  • /debug/pprof/threadcreate - Thread creation profile

Example:

mux.Handle("/debug/pprof/", httpserver.PprofHandler(httpserver.PprofConfig{
    EnableAuth: true,
    Username:   "admin",
    Password:   "secret",
}))

func PrometheusHandler

func PrometheusHandler() http.Handler

PrometheusHandler returns an http.Handler for the /metrics endpoint.

This exposes Prometheus metrics in the standard text format.

Example:

mux.Handle("/metrics", httpserver.PrometheusHandler())

func PrometheusHandlerFor

func PrometheusHandlerFor(opts promhttp.HandlerOpts) http.Handler

PrometheusHandlerFor returns a Prometheus handler with custom options.

Example:

mux.Handle("/metrics", httpserver.PrometheusHandlerFor(opts))

func RegisterPprof

func RegisterPprof(mux *http.ServeMux, cfg PprofConfig)

RegisterPprof registers pprof handlers on the given ServeMux.

Example:

mux := http.NewServeMux()
httpserver.RegisterPprof(mux, httpserver.DefaultPprofConfig())

func RequestIDFromContext

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext extracts the request ID from the context.

Returns an empty string if no request ID is present.

Example:

func myHandler(w http.ResponseWriter, r *http.Request) {
    id := httpserver.RequestIDFromContext(r.Context())
    log.Printf("Processing request: %s", id)
}

func WriteError

func WriteError(w http.ResponseWriter, statusCode int, message string, errors ...Error)

WriteError writes a JSON error response.

Example:

httpserver.WriteError(w, http.StatusBadRequest,
    "validation failed",
    httpserver.Error{Field: "email", Message: "invalid format"},
)

func WriteJSON

func WriteJSON[T any](w http.ResponseWriter, statusCode int, response Response[T])

WriteJSON writes a JSON response with the given status code.

If JSON encoding fails, the error is logged but not returned since HTTP headers have already been written at that point.

Example:

type UserData struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

httpserver.WriteJSON(w, http.StatusOK, Response[UserData]{
    Data:    UserData{ID: 1, Name: "John"},
    Message: "success",
})

func WriteSuccess

func WriteSuccess[T any](w http.ResponseWriter, statusCode int, data T, message string)

WriteSuccess writes a success JSON response with data.

Example:

httpserver.WriteSuccess(w, http.StatusOK, userData, "user retrieved")

Types

type CORSConfig

type CORSConfig struct {
	// AllowedOrigins is a list of origins that are allowed.
	// Use "*" to allow all origins (not recommended for production).
	AllowedOrigins []string

	// AllowedMethods is a list of HTTP methods allowed.
	// Default: GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH
	AllowedMethods []string

	// AllowedHeaders is a list of headers that are allowed in requests.
	AllowedHeaders []string

	// ExposedHeaders is a list of headers that are exposed to the client.
	ExposedHeaders []string

	// AllowCredentials indicates whether credentials (cookies, auth headers)
	// are allowed in cross-origin requests.
	AllowCredentials bool

	// MaxAge is the maximum age (in seconds) of the preflight cache.
	// Default: 86400 (24 hours)
	MaxAge int
}

CORSConfig configures the CORS middleware.

func DefaultCORSConfig

func DefaultCORSConfig() CORSConfig

DefaultCORSConfig returns a permissive CORS configuration.

This is suitable for development. For production, customize the AllowedOrigins to your specific domains.

type CheckResult

type CheckResult struct {
	Status              string `json:"status"`
	Latency             string `json:"latency"`
	Message             string `json:"message,omitempty"`
	LastChecked         string `json:"last_checked"`
	ConsecutiveSuccess  int    `json:"consecutive_successes,omitempty"`
	ConsecutiveFailures int    `json:"consecutive_failures,omitempty"`
}

CheckResult contains the result of a single health check.

type Config

type Config struct {
	// Addr is the TCP address to listen on (default: ":8080").
	Addr string

	// ServiceName is the name of the service (e.g. "my-service").
	// This is used for metrics, tracing, and health checks.
	// Default: "http-server"
	ServiceName string

	// ReadTimeout is the maximum duration for reading the entire request,
	// including the body. A zero or negative value means no timeout.
	//
	// Setting this helps protect against slow-loris attacks where a client
	// sends data very slowly to hold connections open.
	//
	// Default: 15s
	ReadTimeout time.Duration

	// ReadHeaderTimeout is the maximum duration for reading request headers.
	// If zero, ReadTimeout is used. If both are zero, there is no timeout.
	//
	// Default: 10s
	ReadHeaderTimeout time.Duration

	// WriteTimeout is the maximum duration before timing out writes of the response.
	// A zero or negative value means no timeout.
	//
	// Default: 15s
	WriteTimeout time.Duration

	// IdleTimeout is the maximum duration to wait for the next request when
	// keep-alives are enabled. If zero, ReadTimeout is used.
	//
	// Default: 60s
	IdleTimeout time.Duration

	// MaxHeaderBytes controls the maximum number of bytes the server will
	// read parsing the request header's keys and values.
	//
	// Default: 1MB (1 << 20)
	MaxHeaderBytes int

	// TLSConfig optionally provides TLS configuration for HTTPS.
	// If nil, the server runs in HTTP mode.
	TLSConfig *tls.Config

	// Logger configures the server logger.
	Logger zerolog.Logger

	// Middleware is a list of middleware to apply to the server.
	Middleware []Middleware

	// Handler is the HTTP handler to serve requests.
	// This is required and must be set via WithHandler().
	Handler http.Handler

	// ShutdownTimeout is the maximum duration to wait for active connections
	// to complete during graceful shutdown.
	//
	// When shutdown is triggered:
	// 1. Server stops accepting new connections
	// 2. Waits up to ShutdownTimeout for in-flight requests to complete
	// 3. Forcibly closes remaining connections
	//
	// Default: 10s
	ShutdownTimeout time.Duration

	// DrainInterval is the interval between checks during shutdown to see if
	// all connections have been drained.
	//
	// Default: 500ms
	DrainInterval time.Duration

	// TracingConfig enables tracing. If set, ServiceName is automatically applied.
	TracingConfig *TracingConfig

	// MetricsConfig enables metrics. If set, ServiceName is automatically applied.
	MetricsConfig *MetricsConfig

	// LoggerConfig enables request logging. If set, ServiceName is automatically applied.
	LoggerConfig *LoggerConfig

	// HealthHandler is populated by WithHealth with the server's ServiceName.
	HealthHandler **HealthHandler

	// HealthVersion is the version string for health responses.
	HealthVersion string

	// RateLimitConfig enables global rate limiting.
	RateLimitConfig *RateLimitConfig
}

Config holds the HTTP server configuration parameters.

Use DefaultConfig(), ProductionConfig(), or DevelopmentConfig() to get a properly initialized configuration, then modify specific fields as needed.

Example:

cfg := httpserver.DefaultConfig()
cfg.Addr = ":9090"
cfg.ShutdownTimeout = 15 * time.Second

server := httpserver.New(
    httpserver.WithConfig(cfg),
    httpserver.WithHandler(mux),
)

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a balanced configuration suitable for most use cases.

This configuration provides reasonable timeouts that protect against common attacks while being lenient enough for typical web applications.

Timeout values:

  • ReadTimeout: 15s
  • WriteTimeout: 15s
  • IdleTimeout: 60s
  • ShutdownTimeout: 10s

func DevelopmentConfig

func DevelopmentConfig() Config

DevelopmentConfig returns a lenient configuration for local development.

Key differences from DefaultConfig:

  • No read/write timeouts (allows debugging with breakpoints)
  • Longer idle timeout (less connection churn during development)
  • Very short shutdown timeout (fast restart during iteration)

Warning: Do not use this in production!

Timeout values:

  • ReadTimeout: 0 (unlimited)
  • WriteTimeout: 0 (unlimited)
  • IdleTimeout: 120s
  • ShutdownTimeout: 3s

func ProductionConfig

func ProductionConfig() Config

ProductionConfig returns a hardened configuration optimized for production.

Designed for Kubernetes environments where the default terminationGracePeriodSeconds is 30s.

Rationale:

  • ReadTimeout (10s): Most API requests complete in <1s; 10s catches slow clients while failing fast for genuinely stalled connections.
  • WriteTimeout (10s): Matches read; responses should complete quickly.
  • IdleTimeout (30s): Shorter than default to free resources faster; most HTTP/2 clients handle reconnection gracefully.
  • ShutdownTimeout (25s): K8s sends SIGTERM, then waits terminationGracePeriodSeconds (30s) before SIGKILL. We use 25s to complete gracefully with 5s buffer. This allows in-flight requests to finish while ensuring we exit before K8s forcibly kills the pod.

Timeout values:

  • ReadTimeout: 10s
  • WriteTimeout: 10s
  • IdleTimeout: 30s
  • ShutdownTimeout: 25s (5s buffer before K8s SIGKILL at 30s)

type CredentialValidator

type CredentialValidator interface {
	// Validate checks if the client_id and passkey are valid.
	// Returns nil if valid, error if invalid.
	Validate(ctx context.Context, clientID, passkey string) error
}

CredentialValidator validates service credentials.

type CredentialValidatorFunc

type CredentialValidatorFunc func(ctx context.Context, clientID, passkey string) error

CredentialValidatorFunc is an adapter to allow ordinary functions as validators.

func (CredentialValidatorFunc) Validate

func (f CredentialValidatorFunc) Validate(ctx context.Context, clientID, passkey string) error

type Error

type Error struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

Error represents a single field-level error.

type HealthCheck

type HealthCheck func(ctx context.Context) error

HealthCheck is a function that checks the health of a dependency.

Return nil if the dependency is healthy, or an error describing the issue.

Example:

func dbHealthCheck(ctx context.Context) error {
    return db.PingContext(ctx)
}

type HealthHandler

type HealthHandler struct {
	// contains filtered or unexported fields
}

HealthHandler manages health check endpoints.

Create a HealthHandler using NewHealthHandler():

health := httpserver.NewHealthHandler(
    httpserver.WithServiceName("my-service"),
    httpserver.WithVersion("1.0.0"),
)

health.AddReadinessCheck("postgres", dbChecker)
health.AddReadinessCheck("redis", redisChecker)

mux.Handle("/ping", health.PingHandler())
mux.Handle("/livez", health.LiveHandler())
mux.Handle("/readyz", health.ReadyHandler())

func NewHealthHandler

func NewHealthHandler(opts ...HealthOption) *HealthHandler

NewHealthHandler creates a new HealthHandler.

When using with httpserver, use WithHealth instead for automatic ServiceName.

Example (standalone):

health := httpserver.NewHealthHandler(
    httpserver.WithVersion("2.1.0"),
)

Example (with server - recommended):

var health *httpserver.HealthHandler
server := httpserver.New(
    httpserver.WithServiceName("my-api"),
    httpserver.WithHealth(&health, "2.1.0"),
)
// health is now usable with ServiceName auto-set

func (*HealthHandler) AddLivenessCheck

func (h *HealthHandler) AddLivenessCheck(name string, check HealthCheck)

AddLivenessCheck adds a health check for the liveness probe.

Liveness checks determine if the process is alive and not deadlocked. If a liveness check fails, Kubernetes will restart the pod.

Use sparingly - most applications only need basic process checks.

Example:

health.AddLivenessCheck("goroutines", func(ctx context.Context) error {
    if runtime.NumGoroutine() > 10000 {
        return errors.New("too many goroutines")
    }
    return nil
})

func (*HealthHandler) AddReadinessCheck

func (h *HealthHandler) AddReadinessCheck(name string, check HealthCheck)

AddReadinessCheck adds a health check for the readiness probe.

Readiness checks determine if the service can handle traffic. If a readiness check fails, Kubernetes stops routing traffic to the pod but does not restart it.

Add checks for all critical dependencies (database, cache, message queues).

Example:

health.AddReadinessCheck("postgres", func(ctx context.Context) error {
    return db.PingContext(ctx)
})

func (*HealthHandler) LiveHandler

func (h *HealthHandler) LiveHandler() http.Handler

LiveHandler returns an http.Handler for the /livez endpoint.

Returns 200 if all liveness checks pass, 503 otherwise.

func (*HealthHandler) PingHandler

func (h *HealthHandler) PingHandler() http.Handler

PingHandler returns an http.Handler for the /ping endpoint.

This is a simple connectivity check that always returns 200 OK. It does not run any health checks.

func (*HealthHandler) ReadyHandler

func (h *HealthHandler) ReadyHandler() http.Handler

ReadyHandler returns an http.Handler for the /readyz endpoint.

Returns 200 if all readiness checks pass, 503 otherwise.

type HealthOption

type HealthOption func(*HealthHandler)

HealthOption configures the HealthHandler.

func WithVersion

func WithVersion(version string) HealthOption

WithVersion sets the version for health responses.

type HealthResponse

type HealthResponse struct {
	Status    string                 `json:"status"`
	Service   string                 `json:"service"`
	Version   string                 `json:"version"`
	Uptime    string                 `json:"uptime,omitempty"`
	Hostname  string                 `json:"hostname,omitempty"`
	Timestamp string                 `json:"timestamp"`
	Checks    map[string]CheckResult `json:"checks,omitempty"`
}

HealthResponse contains the full health response data.

type KeyFunc

type KeyFunc func(r *http.Request) string

KeyFunc extracts a rate limiting key from a request.

The key determines how requests are grouped for rate limiting. Requests with the same key share the same rate limit bucket.

Example Usage

Global rate limit (all requests share one bucket):

httpserver.RateLimit(httpserver.RateLimitConfig{
    Limit:   100,    // 100 requests per second
    Burst:   200,    // Allow bursts up to 200
    KeyFunc: nil,    // nil = global (all requests share bucket)
})

Per-IP rate limit (each IP has its own bucket):

httpserver.RateLimit(httpserver.RateLimitConfig{
    Limit:   100,
    Burst:   200,
    KeyFunc: httpserver.KeyFuncByIP(), // Each IP gets 100 req/sec
})

Per-endpoint rate limit (each endpoint has its own bucket):

httpserver.RateLimit(httpserver.RateLimitConfig{
    Limit:   100,
    Burst:   200,
    KeyFunc: httpserver.KeyFuncByPath(), // /api/users and /api/orders limited separately
})

Per-IP per-endpoint rate limit (most granular):

httpserver.RateLimit(httpserver.RateLimitConfig{
    Limit:   100,
    Burst:   200,
    KeyFunc: httpserver.KeyFuncByIPAndPath(), // IP1:/api/users != IP1:/api/orders != IP2:/api/users
})

func KeyFuncByClientID

func KeyFuncByClientID() KeyFunc

KeyFuncByClientID returns a KeyFunc that uses the client_id from ServiceAuth.

Use Case

Rate limit authenticated service clients. Use after ServiceAuth middleware.

Example

// Each authenticated client gets 1000 req/sec across all endpoints
mux.Handle("/internal/", httpserver.Chain(
    httpserver.ServiceAuth(authConfig),
    httpserver.RateLimit(httpserver.RateLimitConfig{
        Limit:   1000,
        Burst:   2000,
        KeyFunc: httpserver.KeyFuncByClientID(),
    }),
)(handler))

Prerequisite

Must be used after ServiceAuth middleware. Without authentication, returns empty string (all unauthenticated requests share one bucket).

func KeyFuncByClientIDAndPath

func KeyFuncByClientIDAndPath() KeyFunc

KeyFuncByClientIDAndPath combines client_id and path.

Use Case

Rate limit authenticated clients per endpoint. For example, a client might have different limits for read vs write operations.

Example

// client-1 gets 100 req/sec to /api/read
// client-1 gets separate 10 req/sec to /api/write
mux.Handle("/api/", httpserver.Chain(
    httpserver.ServiceAuth(authConfig),
    httpserver.RateLimit(httpserver.RateLimitConfig{
        Limit:   100,
        Burst:   200,
        KeyFunc: httpserver.KeyFuncByClientIDAndPath(),
    }),
)(handler))

func KeyFuncByHeader

func KeyFuncByHeader(header string) KeyFunc

KeyFuncByHeader returns a KeyFunc that extracts a value from a header.

Use Case

Rate limit by a custom header, such as tenant ID or API key.

Example

// Each tenant gets 100 req/sec
mux.Handle("/api/", httpserver.RateLimit(httpserver.RateLimitConfig{
    Limit:   100,
    Burst:   200,
    KeyFunc: httpserver.KeyFuncByHeader("X-Tenant-ID"),
})(handler))

func KeyFuncByIP

func KeyFuncByIP() KeyFunc

KeyFuncByIP returns a KeyFunc that extracts the client IP.

Uses X-Forwarded-For header if present (for reverse proxy setups), otherwise falls back to RemoteAddr.

Use Case

Rate limit each client IP independently. For example, 100 req/sec per IP means one abusive client doesn't affect other clients.

Example

// Each IP can make 100 requests per second
mux.Handle("/api/", httpserver.RateLimit(httpserver.RateLimitConfig{
    Limit:   100,
    Burst:   200,
    KeyFunc: httpserver.KeyFuncByIP(),
})(handler))

func KeyFuncByIPAndPath

func KeyFuncByIPAndPath() KeyFunc

KeyFuncByIPAndPath returns a KeyFunc that combines client IP and path.

Use Case

Most granular rate limiting for public APIs. Each client IP gets its own rate limit bucket per endpoint.

Example

// IP 1.2.3.4 gets 100 req/sec to /api/users
// IP 1.2.3.4 gets separate 100 req/sec to /api/orders
// IP 5.6.7.8 gets its own 100 req/sec to /api/users
mux.Handle("/api/", httpserver.RateLimit(httpserver.RateLimitConfig{
    Limit:   100,
    Burst:   200,
    KeyFunc: httpserver.KeyFuncByIPAndPath(),
})(handler))

func KeyFuncByPath

func KeyFuncByPath() KeyFunc

KeyFuncByPath returns a KeyFunc that uses the URL path.

Use Case

Rate limit each endpoint independently. For example, /api/search might have a lower limit than /api/users because it's more expensive.

Example

// All requests to /api/search share 10 req/sec
// All requests to /api/users share separate 100 req/sec
mux.Handle("/api/", httpserver.RateLimit(httpserver.RateLimitConfig{
    Limit:   100,
    Burst:   200,
    KeyFunc: httpserver.KeyFuncByPath(),
})(handler))

Note

This limits ALL clients combined per endpoint. For per-client per-endpoint, use KeyFuncByIPAndPath or KeyFuncByClientIDAndPath.

type Limiter

type Limiter interface {
	Limit(ctx context.Context) (time.Duration, error)
}

Limiter is an interface for rate limiters (useful for testing).

type LoggerConfig

type LoggerConfig struct {
	Logger zerolog.Logger

	// SkipPaths are paths that should not be logged.
	// Useful for health check endpoints that are called frequently.
	SkipPaths []string

	// LogRequestBody enables logging of request body (use with caution).
	// This reads the entire request body into memory, which may impact
	// performance for large payloads. Consider using only in development.
	LogRequestBody bool

	// LogResponseBody enables logging of response body (use with caution).
	// This buffers the entire response body, which may impact performance
	// for large payloads. Consider using only in development.
	LogResponseBody bool

	// MaxBodyLogSize limits the size of logged bodies (default: 4KB).
	// Bodies larger than this will be truncated in the log output.
	MaxBodyLogSize int
	// contains filtered or unexported fields
}

LoggerConfig configures the logging middleware.

type MemoryCredentialValidator

type MemoryCredentialValidator struct {
	// contains filtered or unexported fields
}

MemoryCredentialValidator validates against an in-memory map. Useful for credentials loaded from environment variables.

func NewMemoryCredentialValidator

func NewMemoryCredentialValidator(clients map[string]string) *MemoryCredentialValidator

NewMemoryCredentialValidator creates a validator from a map of client_id -> passkey.

Example:

validator := httpserver.NewMemoryCredentialValidator(map[string]string{
    os.Getenv("SERVICE_CLIENT_ID"): os.Getenv("SERVICE_PASS_KEY"),
})

func (*MemoryCredentialValidator) Validate

func (v *MemoryCredentialValidator) Validate(_ context.Context, clientID, passkey string) error

type Metrics

type Metrics struct {
	// contains filtered or unexported fields
}

Metrics provides server metrics using OpenTelemetry.

func NewMetrics

func NewMetrics(cfg MetricsConfig) (*Metrics, error)

NewMetrics creates a new Metrics instance.

func (*Metrics) Middleware

func (m *Metrics) Middleware() Middleware

Middleware returns middleware that records HTTP metrics.

Metrics recorded:

  • http.server.request.duration: Request latency histogram
  • http.server.request.size: Request body size histogram
  • http.server.response.size: Response body size histogram
  • http.server.active_requests: In-flight request gauge
  • http.server.request.total: Total request counter
  • http.server.response.status: Status code distribution

Example:

metrics, _ := httpserver.NewMetrics(httpserver.DefaultMetricsConfig())
handler := metrics.Middleware()(myHandler)

type MetricsConfig

type MetricsConfig struct {
	// MeterProvider is the OTel meter provider.
	// If nil, uses otel.GetMeterProvider().
	MeterProvider metric.MeterProvider

	// SkipPaths are paths that should not be recorded.
	SkipPaths []string

	// Buckets for request duration histogram (in seconds).
	// Default: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
	DurationBuckets []float64
	// contains filtered or unexported fields
}

MetricsConfig configures the metrics middleware.

func DefaultMetricsConfig

func DefaultMetricsConfig() MetricsConfig

DefaultMetricsConfig returns a default metrics configuration.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware is a function that wraps an http.Handler.

Middleware functions are composed together using Chain() to create a processing pipeline for HTTP requests.

Example:

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("Request: %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

func CORS

func CORS(cfg CORSConfig) Middleware

CORS returns middleware that handles Cross-Origin Resource Sharing.

Example:

handler := httpserver.CORS(httpserver.CORSConfig{
    AllowedOrigins:   []string{"https://example.com"},
    AllowCredentials: true,
})(myHandler)

func Chain

func Chain(middlewares ...Middleware) Middleware

Chain composes multiple middleware into a single middleware.

Middleware are applied in the order provided. The first middleware is the outermost (runs first on request, last on response).

Example:

handler := httpserver.Chain(
    httpserver.Tracing(tp),
    httpserver.Recovery(),
    httpserver.Logger(logger),
)(myHandler)

Request flow:

Tracing -> Recovery -> Logger -> myHandler -> Logger -> Recovery -> Tracing

func DefaultMiddleware

func DefaultMiddleware(opts ...MiddlewareOption) Middleware

DefaultMiddleware returns a production-ready middleware stack.

The stack includes (in order):

  1. Recovery - Panic recovery
  2. RequestID - X-Request-ID generation/forwarding
  3. Logger - Request/response logging (if logger provided)

For full observability, add Tracing() middleware manually with your TracerProvider.

Example:

handler := httpserver.Tracing(tp)(
    httpserver.DefaultMiddleware(logger)(myHandler),
)

func Logger

func Logger(cfg LoggerConfig) Middleware

Logger returns middleware that logs HTTP requests.

Logs include:

  • Method, path, status code
  • Request duration
  • Request ID (if present)
  • Client IP
  • Request/Response bodies (if enabled)

Example:

handler := httpserver.Logger(httpserver.LoggerConfig{
    Logger:    logger,
    SkipPaths: []string{"/livez", "/readyz", "/ping"},
})(myHandler)

func RateLimit

func RateLimit(cfg RateLimitConfig) Middleware

RateLimit returns middleware that limits request rate using token bucket algorithm.

func RateLimitByIP

func RateLimitByIP(limit rate.Limit, burst int) Middleware

RateLimitByIP returns rate limiting middleware keyed by client IP.

func RateLimitByIPRedis

func RateLimitByIPRedis(rdb redis.UniversalClient, limit rate.Limit, burst int) Middleware

RateLimitByIPRedis returns distributed rate limiting middleware keyed by client IP.

func Recovery

func Recovery(logger zerolog.Logger) Middleware

Recovery returns middleware that recovers from panics.

When a panic occurs:

  • The panic is recovered
  • A 500 Internal Server Error is returned
  • The stack trace is logged (if logger provided)
  • The request continues to the next middleware

Example:

handler := httpserver.Recovery(logger)(myHandler)

func RequestID

func RequestID() Middleware

RequestID returns middleware that generates or forwards request IDs.

Behavior:

  • If X-Request-ID header exists, use it
  • Otherwise, generate a new UUID v4
  • Add the ID to the response header
  • Store the ID in the request context

Example:

handler := httpserver.RequestID()(myHandler)

// Access in handler:
func myHandler(w http.ResponseWriter, r *http.Request) {
    id := httpserver.RequestIDFromContext(r.Context())
    log.Printf("Request ID: %s", id)
}

func ServiceAuth

func ServiceAuth(cfg ServiceAuthConfig) Middleware

ServiceAuth returns middleware that validates service-to-service credentials.

Requires both Client-ID and Pass-Key headers to be present and valid. On failure, returns 401 Unauthorized with a generic error message.

Example with in-memory validator:

validator := httpserver.NewMemoryCredentialValidator(map[string]string{
    os.Getenv("CLIENT_ID"): os.Getenv("PASS_KEY"),
})

mux.Handle("/internal/", httpserver.ServiceAuth(httpserver.ServiceAuthConfig{
    Validator: validator,
})(internalHandler))

Example with database validator:

validator := httpserver.NewSQLCredentialValidator(db,
    "SELECT passkey FROM service_credentials WHERE client_id = $1 AND is_active = true",
)

mux.Handle("/internal/", httpserver.ServiceAuth(httpserver.ServiceAuthConfig{
    Validator: validator,
})(internalHandler))

func Timeout

func Timeout(timeout time.Duration) Middleware

Timeout returns middleware that limits request processing time.

If the handler takes longer than the timeout, the request context is cancelled and a 503 Service Unavailable response is returned.

Note: The handler must respect context cancellation for this to work effectively.

Example:

handler := httpserver.Timeout(30 * time.Second)(myHandler)

func Tracing

func Tracing(cfg TracingConfig) Middleware

Tracing returns middleware that adds OpenTelemetry tracing to requests.

Features:

  • Extracts trace context from incoming requests (W3C TraceContext)
  • Creates server spans with standard HTTP attributes
  • Records errors on 5xx responses
  • Propagates span context to downstream handlers

Example:

handler := httpserver.Tracing(httpserver.TracingConfig{
    TracerProvider: tp,
    ServiceName:    "api-gateway",
})(myHandler)

type MiddlewareOption

type MiddlewareOption func(*middlewareConfig)

MiddlewareOption configures DefaultMiddleware.

func WithDefaultLogger

func WithDefaultLogger(cfg LoggerConfig) MiddlewareOption

WithDefaultLogger adds logging middleware to DefaultMiddleware.

type Option

type Option func(*Config)

Option configures the server.

func WithConfig

func WithConfig(cfg Config) Option

WithConfig applies all settings from a Config struct.

This is the recommended way to configure the server. Use one of the preset configurations (DefaultConfig, ProductionConfig, DevelopmentConfig) as a starting point, then override specific fields as needed.

Example:

cfg := httpserver.ProductionConfig()
cfg.Addr = ":9090"
cfg.ShutdownTimeout = 30 * time.Second

server := httpserver.New(
    httpserver.WithConfig(cfg),
    httpserver.WithServiceName("my-api"),
    httpserver.WithHandler(mux),
)

func WithHandler

func WithHandler(h http.Handler) Option

WithHandler sets the HTTP handler for the server.

This is required. The handler will be wrapped with any configured middleware in the order they are specified.

Example:

mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)

server := httpserver.New(
    httpserver.WithHandler(mux),
)

func WithHealth

func WithHealth(handler **HealthHandler, version string) Option

WithHealth enables health check endpoints with auto-configured ServiceName.

This creates a HealthHandler with the server's ServiceName and version, and returns it for adding checks and registering routes.

Example:

var health *httpserver.HealthHandler
server := httpserver.New(
    httpserver.WithServiceName("my-api"),
    httpserver.WithHealth(&health, "1.0.0"),
    httpserver.WithHandler(mux),
)

health.AddReadinessCheck("database", dbPingCheck)
mux.Handle("/ping", health.PingHandler())
mux.Handle("/livez", health.LiveHandler())
mux.Handle("/readyz", health.ReadyHandler())

func WithLogger

func WithLogger(l zerolog.Logger) Option

WithLogger sets the server logger for lifecycle events only.

This logger is used for:

  • Server startup messages
  • Graceful shutdown logging
  • Internal errors

For REQUEST logging (each HTTP request/response), use WithLogging instead. You can use both: WithLogger for server events, WithLogging for requests.

Example:

logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
server := httpserver.New(
    httpserver.WithLogger(logger),  // Server events
    httpserver.WithLogging(httpserver.LoggerConfig{Logger: logger}),  // Requests
    httpserver.WithHandler(mux),
)

func WithLogging

func WithLogging(cfg LoggerConfig) Option

WithLogging enables request logging middleware.

The server's ServiceName is automatically included in all log entries. You don't need to set ServiceName in LoggerConfig.

Example:

logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
server := httpserver.New(
    httpserver.WithServiceName("my-api"),
    httpserver.WithLogging(httpserver.LoggerConfig{
        Logger:    logger,
        SkipPaths: []string{"/livez", "/readyz", "/ping"},
    }),
    httpserver.WithHandler(mux),
)

func WithMetrics

func WithMetrics(cfg MetricsConfig) Option

WithMetrics enables OpenTelemetry metrics middleware.

The server's ServiceName is automatically applied to all metrics. You don't need to set ServiceName in MetricsConfig.

Example:

server := httpserver.New(
    httpserver.WithServiceName("my-api"),
    httpserver.WithMetrics(httpserver.MetricsConfig{
        SkipPaths: []string{"/livez", "/readyz", "/ping"},
    }),
    httpserver.WithHandler(mux),
)

func WithMiddleware

func WithMiddleware(ms ...Middleware) Option

WithMiddleware adds middleware to wrap the handler.

Middleware is applied in order (first middleware wraps outermost). Use this for custom middleware or when you need fine-grained control.

For built-in middleware (tracing, metrics, logging), prefer the dedicated options (WithTracing, WithMetrics, WithLogging) which automatically inject the server's ServiceName.

Example:

server := httpserver.New(
    httpserver.WithHandler(mux),
    httpserver.WithMiddleware(
        httpserver.Recovery(logger),
        httpserver.RequestID(),
        httpserver.CORS(corsConfig),
    ),
)

func WithRateLimit

func WithRateLimit(cfg RateLimitConfig) Option

WithRateLimit enables global rate limiting for all requests.

For per-endpoint rate limiting, use the RateLimit middleware directly on specific routes instead.

Example (global rate limit):

server := httpserver.New(
    httpserver.WithRateLimit(httpserver.RateLimitConfig{
        Limit: 100,  // 100 requests per second
        Burst: 200,  // Allow bursts up to 200
    }),
    httpserver.WithHandler(mux),
)

Example (per-endpoint rate limit - add middleware to specific routes):

// Apply stricter limit to sensitive endpoint
mux.Handle("/api/login", httpserver.RateLimitByIP(10, 20)(loginHandler))

// Apply different limit to API endpoints
mux.Handle("/api/", httpserver.RateLimit(httpserver.RateLimitConfig{
    Limit: 50,
    Burst: 100,
})(apiHandler))

func WithServiceName

func WithServiceName(name string) Option

WithServiceName sets the service name for the entire server.

This is the SINGLE source of truth for service identity. The server automatically passes this value to all components that need it:

  • Tracing spans (service.name attribute)
  • Metrics (service.name label)
  • Request logs (service field)
  • Health check responses (service field)

Example:

server := httpserver.New(
    httpserver.WithServiceName("payment-api"),
    httpserver.WithHandler(mux),
)

func WithTracing

func WithTracing(cfg TracingConfig) Option

WithTracing enables OpenTelemetry tracing middleware.

The server's ServiceName is automatically applied to all spans. You don't need to set ServiceName in TracingConfig.

Example:

server := httpserver.New(
    httpserver.WithServiceName("my-api"),
    httpserver.WithTracing(httpserver.TracingConfig{
        SkipPaths: []string{"/livez", "/readyz", "/ping"},
    }),
    httpserver.WithHandler(mux),
)

type PingResponse

type PingResponse struct {
	Status string `json:"status"`
}

PingResponse contains the ping response data.

type PprofConfig

type PprofConfig struct {
	// Prefix is the URL prefix for pprof endpoints.
	// Default: "/debug/pprof"
	Prefix string

	// EnableAuth enables basic authentication for pprof endpoints.
	EnableAuth bool

	// Username for basic auth (required when EnableAuth is true).
	Username string

	// Password for basic auth (required when EnableAuth is true).
	Password string
}

PprofConfig configures pprof endpoint security.

func DefaultPprofConfig

func DefaultPprofConfig() PprofConfig

DefaultPprofConfig returns default pprof configuration.

type RateLimitConfig

type RateLimitConfig struct {
	// Limit is the rate limit in requests per second.
	Limit rate.Limit

	// Burst is the maximum burst size (token bucket capacity).
	Burst int

	// KeyFunc extracts a key from the request for per-key rate limiting.
	// If nil, a global rate limit is applied to all requests.
	KeyFunc KeyFunc

	// Redis enables distributed rate limiting across multiple instances.
	// If nil, an in-memory rate limiter is used (single-instance only).
	Redis redis.UniversalClient

	// RedisKeyPrefix is the prefix for Redis keys.
	// Default: "ratelimit:"
	RedisKeyPrefix string

	// WindowDuration is the sliding window duration for Redis rate limiting.
	// Default: 1 second
	WindowDuration time.Duration
}

RateLimitConfig configures the rate limiting middleware.

func DefaultRateLimitConfig

func DefaultRateLimitConfig() RateLimitConfig

DefaultRateLimitConfig returns a default rate limit configuration.

type Response

type Response[T any] struct {
	Data    T       `json:"data,omitempty"`
	Errors  []Error `json:"errors,omitempty"`
	Message string  `json:"message,omitempty"`
}

Response is a generic wrapper for all API responses.

This provides a consistent structure across all endpoints:

  • Data: The actual response payload (type-safe via generics)
  • Errors: List of field-level errors (for validation failures)
  • Message: Human-readable message

Example success response:

{
  "data": {"id": 123, "name": "John"},
  "message": "user created"
}

Example error response:

{
  "errors": [{"field": "email", "message": "invalid format"}],
  "message": "validation failed"
}

type SQLCredentialValidator

type SQLCredentialValidator struct {
	// contains filtered or unexported fields
}

SQLCredentialValidator validates against a database table. Expects a table with client_id and passkey columns.

func NewSQLCredentialValidator

func NewSQLCredentialValidator(db *sql.DB, query string) *SQLCredentialValidator

NewSQLCredentialValidator creates a validator that queries a database.

The query should return the passkey for a given client_id. Example query: "SELECT passkey FROM service_credentials WHERE client_id = $1 AND is_active = true"

Example:

validator := httpserver.NewSQLCredentialValidator(
    db,
    "SELECT passkey FROM service_credentials WHERE client_id = $1",
)

func (*SQLCredentialValidator) Validate

func (v *SQLCredentialValidator) Validate(ctx context.Context, clientID, passkey string) error

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server wraps http.Server with graceful shutdown, signal handling, and lifecycle logging.

Create a Server using New():

server := httpserver.New(
    httpserver.WithConfig(httpserver.ProductionConfig()),
    httpserver.WithServiceName("my-api"),
    httpserver.WithHandler(mux),
)

// Blocks until shutdown signal (SIGTERM, SIGINT) or context cancellation
if err := server.ListenAndServe(ctx); err != nil {
    log.Fatal(err)
}

func New

func New(opts ...Option) *Server

New creates a new Server with the provided options.

At minimum, you must provide a handler using WithHandler(). If no config is provided, DefaultConfig() is used.

Example:

server := httpserver.New(
    httpserver.WithServiceName("my-api"),
    httpserver.WithHandler(mux),
    httpserver.WithMiddleware(
        httpserver.Recovery(logger),
        httpserver.RequestID(),
    ),
)

func (*Server) Addr

func (s *Server) Addr() string

Addr returns the server's listen address.

This is useful when using ":0" to let the OS pick a random port. Note: This returns the configured address, not the actual bound address. For the actual address after binding, check the listener.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(ctx context.Context) error

ListenAndServe starts the server and blocks until shutdown.

The server will shut down gracefully when:

  • The provided context is cancelled
  • SIGTERM or SIGINT is received

During shutdown:

  1. Server stops accepting new connections
  2. Waits up to ShutdownTimeout for in-flight requests
  3. Returns nil on clean shutdown, or error if shutdown times out

Example:

ctx := context.Background()
if err := server.ListenAndServe(ctx); err != nil {
    log.Fatal(err)
}

func (*Server) ListenAndServeTLS

func (s *Server) ListenAndServeTLS(ctx context.Context, certFile, keyFile string) error

ListenAndServeTLS starts the server with TLS and blocks until shutdown.

This is equivalent to ListenAndServe but uses the provided certificate and key files for TLS.

Example:

ctx := context.Background()
if err := server.ListenAndServeTLS(ctx, "cert.pem", "key.pem"); err != nil {
    log.Fatal(err)
}

func (*Server) ServiceName

func (s *Server) ServiceName() string

ServiceName returns the configured service name.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown initiates graceful shutdown of the server.

This is useful when you want to trigger shutdown programmatically instead of waiting for signals.

Example:

// In another goroutine or signal handler
if err := server.Shutdown(ctx); err != nil {
    log.Printf("shutdown error: %v", err)
}

type ServiceAuthConfig

type ServiceAuthConfig struct {
	// Validator checks if the client_id and passkey are valid.
	Validator CredentialValidator

	// ClientIDHeader is the header name for client ID.
	// Default: "Client-ID"
	ClientIDHeader string

	// PassKeyHeader is the header name for passkey.
	// Default: "Pass-Key"
	PassKeyHeader string
}

ServiceAuthConfig configures the service-to-service authentication middleware.

type TracingConfig

type TracingConfig struct {
	// TracerProvider is the OTel tracer provider.
	// If nil, uses otel.GetTracerProvider().
	TracerProvider trace.TracerProvider

	// Propagator is the context propagator.
	// If nil, uses otel.GetTextMapPropagator().
	Propagator propagation.TextMapPropagator

	// SkipPaths are paths that should not be traced.
	SkipPaths []string

	// SpanNameFormatter formats the span name.
	// Default: "HTTP {method} {path}"
	SpanNameFormatter func(r *http.Request) string
	// contains filtered or unexported fields
}

TracingConfig configures the tracing middleware.

func DefaultTracingConfig

func DefaultTracingConfig() TracingConfig

DefaultTracingConfig returns a default tracing configuration.

Directories

Path Synopsis
adapters
echo
Package echo provides middleware adapters for Echo framework.
Package echo provides middleware adapters for Echo framework.
fiber
Package fiber provides middleware adapters for Fiber framework.
Package fiber provides middleware adapters for Fiber framework.
gin
Package gin provides middleware adapters for Gin framework.
Package gin provides middleware adapters for Gin framework.
grpcgateway
Package grpcgateway provides middleware adapters for grpc-gateway.
Package grpcgateway provides middleware adapters for grpc-gateway.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL