Documentation
¶
Overview ¶
Package middleware provides HTTP middleware as func(http.Handler) http.Handler, the form the standard library and every stdlib-compatible router understand.
It covers the cross-cutting concerns a service needs before its own handlers matter: CORS, distributed tracing, rate limiting, request logging, Prometheus metrics, and service-key authentication. Identity and the matched route travel on the request context behind typed accessors rather than in a framework-specific bag of values.
Index ¶
- Constants
- Variables
- func CORS(opts CORSOptions) func(http.Handler) http.Handler
- func CORSWithOrigins(allowedOrigins []string) func(http.Handler) http.Handler
- func ClientIP(r *http.Request) string
- func ClientIdentifier(r *http.Request) string
- func HasScope(ctx context.Context, scope string) bool
- func HealthHandler(serviceName string, checks ...HealthChecker) http.Handler
- func IsOperationalPath(path string) bool
- func IsServiceAuthenticated(ctx context.Context) bool
- func Metrics(serviceName string) func(http.Handler) http.Handler
- func MetricsHandler() http.Handler
- func ObserveRequest(serviceName, method, route string, status int, d time.Duration)
- func OptionalServiceAuth(validator ServiceKeyValidator) func(http.Handler) http.Handler
- func QuietSuccessfulProbes(r *http.Request, status int) bool
- func RateLimit(redisClient *rediscluster.ClusterClient, serviceName string) func(http.Handler) http.Handler
- func ReadinessHandler(serviceName string, checks ...HealthChecker) http.Handler
- func RecordRateLimitCheck(serviceName, endpointType, result string)
- func RecordRateLimitError(serviceName, endpointType, errorMsg string)
- func RecordRateLimitExceeded(serviceName, endpointType, clientID string)
- func RequestLogging(serviceName string) func(http.Handler) http.Handler
- func RequestLoggingTo(logger *slog.Logger, serviceName string) func(http.Handler) http.Handler
- func RequestLoggingWith(opts LoggingOptions) func(http.Handler) http.Handler
- func RequireServiceScope(requiredScope string) func(http.Handler) http.Handler
- func Route(r *http.Request) string
- func ServiceAuthOrUserAuth(validator ServiceKeyValidator, userAuth func(http.Handler) http.Handler) func(http.Handler) http.Handler
- func ServiceAuthRequired(validator ServiceKeyValidator) func(http.Handler) http.Handler
- func ServiceKeyFromRequest(r *http.Request) string
- func ServiceName(ctx context.Context) (string, bool)
- func TraceFromRequest(r *http.Request) *httpclient.TraceContext
- func Tracing() func(http.Handler) http.Handler
- func TrackInFlight(serviceName string) func()
- func UserID(ctx context.Context) (string, bool)
- func WithAuthType(ctx context.Context, at AuthType) context.Context
- func WithRoute(ctx context.Context, pattern string) context.Context
- func WithRouteFunc(ctx context.Context, resolve func() string) context.Context
- func WithServiceIdentity(ctx context.Context, info *ServiceKeyInfo) context.Context
- func WithUserID(ctx context.Context, id string) context.Context
- type AuthType
- type CORSOptions
- type CheckResult
- type DatabaseHealthChecker
- type DatabaseMetrics
- type EndpointType
- type EventBusHealthChecker
- type HealthCheckResponse
- type HealthChecker
- type LoggingOptions
- type RateLimitConfig
- type RateLimiter
- type ServiceHealthChecker
- type ServiceKeyInfo
- type ServiceKeyValidator
Examples ¶
Constants ¶
const ( StatusHealthy = "healthy" StatusUnhealthy = "unhealthy" )
StatusHealthy and StatusUnhealthy are the values a HealthChecker reports.
const APIKeyHeader = "X-API-Key"
APIKeyHeader is the primary header carrying a service API key. A key presented as a bearer token is accepted as well.
const MetricsPath = "/metrics"
MetricsPath is the endpoint serving the Prometheus exposition format. The middleware skips it so scrapes do not inflate a service's own request counts.
const ScopeAll = "all"
ScopeAll is the wildcard scope that satisfies any RequireScope check.
Variables ¶
var DefaultCORSHeaders = []string{
"Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token",
"Authorization", "accept", "origin", "Cache-Control", "X-Requested-With",
"X-Trace-ID", "X-User-ID", "traceparent",
}
DefaultCORSHeaders is the request-header allowlist applied when CORSOptions leaves AllowedHeaders empty.
var DefaultCORSMethods = []string{ http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodOptions, }
DefaultCORSMethods is the method allowlist applied when CORSOptions leaves AllowedMethods empty.
var RateLimitConfigs = map[EndpointType]RateLimitConfig{ EndpointTypeAuth: { RequestsPerMinute: 10, BurstAllowance: 3, BlockDuration: 5 * time.Minute, }, EndpointTypeData: { RequestsPerMinute: 60, BurstAllowance: 10, BlockDuration: 1 * time.Minute, }, EndpointTypeUpload: { RequestsPerMinute: 20, BurstAllowance: 5, BlockDuration: 2 * time.Minute, }, EndpointTypeAdmin: { RequestsPerMinute: 30, BurstAllowance: 5, BlockDuration: 1 * time.Minute, }, EndpointTypePublic: { RequestsPerMinute: 100, BurstAllowance: 20, BlockDuration: 30 * time.Second, }, EndpointTypeInternal: { RequestsPerMinute: 200, BurstAllowance: 50, BlockDuration: 30 * time.Second, }, EndpointTypeHealthy: { RequestsPerMinute: 0, BurstAllowance: 0, BlockDuration: 0, }, }
RateLimitConfigs holds rate limit configurations for different endpoint types
Functions ¶
func CORS ¶
func CORS(opts CORSOptions) func(http.Handler) http.Handler
CORS returns a middleware applying opts to every request and answering preflight requests with 204.
Example ¶
Middleware is func(http.Handler) http.Handler, so it composes with the standard library and with any router that speaks the same shape.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/dobrevit/svckit/middleware"
)
func main() {
routes := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ok")
})
handler := middleware.CORSWithOrigins([]string{"https://app.example.com"})(routes)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/orders", nil)
r.Header.Set("Origin", "https://app.example.com")
handler.ServeHTTP(w, r)
fmt.Println(w.Header().Get("Access-Control-Allow-Origin"))
}
Output: https://app.example.com
Example (Preflight) ¶
A preflight request is answered by the middleware and never reaches the handler behind it.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/dobrevit/svckit/middleware"
)
func main() {
reached := false
routes := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { reached = true })
handler := middleware.CORSWithOrigins([]string{"https://app.example.com"})(routes)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodOptions, "/orders", nil)
r.Header.Set("Origin", "https://app.example.com")
handler.ServeHTTP(w, r)
fmt.Println(w.Code, reached)
}
Output: 204 false
func CORSWithOrigins ¶
CORSWithOrigins returns a credentialed CORS middleware restricted to allowedOrigins, using the default method and header allowlists.
func ClientIP ¶
ClientIP returns the originating client address, preferring the left-most entry of X-Forwarded-For, then X-Real-IP, then the transport's remote address.
Both headers are trivially forgeable by a direct caller, so this is only trustworthy behind a proxy that overwrites them. Callers exposed directly to the internet should not treat the result as an identity.
func ClientIdentifier ¶
ClientIdentifier returns the rate-limiting bucket for r: the authenticated user, else the calling service, else the client address. Buckets are prefixed by kind so a user ID can never collide with a service name.
func HasScope ¶
HasScope reports whether the authenticated service holds scope. The wildcard scope "all" satisfies every check.
func HealthHandler ¶
func HealthHandler(serviceName string, checks ...HealthChecker) http.Handler
HealthHandler serves a liveness report for serviceName: the outcome of every check, with 503 when any of them is unhealthy.
func IsOperationalPath ¶ added in v0.1.2
IsOperationalPath reports whether path is polled by infrastructure rather than requested by a user: liveness, readiness, metrics and profiling.
func IsServiceAuthenticated ¶
IsServiceAuthenticated reports whether ctx was authenticated as a service.
func Metrics ¶
Metrics returns a middleware recording request count, duration and in-flight gauge for serviceName.
The route label comes from Route, so it is the templated pattern when the router adapter recorded one. Without that, an ID-bearing path produces one label value per ID and the series count grows without bound.
func MetricsHandler ¶
MetricsHandler serves the Prometheus exposition format.
func ObserveRequest ¶
ObserveRequest records one completed HTTP request. It is exported so that router-specific adapters, which capture the status themselves, record through the same series as the middleware.
func OptionalServiceAuth ¶
func OptionalServiceAuth(validator ServiceKeyValidator) func(http.Handler) http.Handler
OptionalServiceAuth returns a middleware that validates a service API key when one is presented and lets unauthenticated requests through untouched. A key that is presented but invalid is still rejected.
func QuietSuccessfulProbes ¶ added in v0.1.2
QuietSuccessfulProbes demotes successful requests to the operational endpoints from info to debug. Under an orchestrator these are the bulk of a service's log volume and none of its information.
A probe that fails stays at info: a readiness check that starts flapping is precisely the thing worth seeing in normal output, and it is the worst possible moment to have silenced it.
func RateLimit ¶
func RateLimit(redisClient *rediscluster.ClusterClient, serviceName string) func(http.Handler) http.Handler
RateLimit returns a middleware enforcing the per-endpoint-type limits in RateLimitConfigs, backed by redisClient.
A rate limiter that cannot reach Redis lets the request through: an unreachable limiter should not take the service down with it. That choice means a Redis outage removes rate limiting rather than traffic.
func ReadinessHandler ¶
func ReadinessHandler(serviceName string, checks ...HealthChecker) http.Handler
ReadinessHandler serves a readiness report for serviceName, in the shape Kubernetes readiness probes consume: 200 while every check passes, 503 otherwise.
func RecordRateLimitCheck ¶
func RecordRateLimitCheck(serviceName, endpointType, result string)
RecordRateLimitCheck records a rate limit check
func RecordRateLimitError ¶
func RecordRateLimitError(serviceName, endpointType, errorMsg string)
RecordRateLimitError records a rate limiting error
func RecordRateLimitExceeded ¶
func RecordRateLimitExceeded(serviceName, endpointType, clientID string)
RecordRateLimitExceeded records a rate limit violation
func RequestLogging ¶
RequestLogging returns a middleware that logs one line per completed request through slog's default logger, quieting successful probes.
func RequestLoggingTo ¶
RequestLoggingTo is RequestLogging against a specific logger.
func RequestLoggingWith ¶ added in v0.1.2
func RequestLoggingWith(opts LoggingOptions) func(http.Handler) http.Handler
RequestLoggingWith returns a middleware that logs one line per completed request: method, route, status, size, duration and client address.
func RequireServiceScope ¶
RequireServiceScope returns a middleware that rejects an authenticated service lacking requiredScope. It must run after ServiceAuthRequired.
func Route ¶
Route returns the matched route pattern recorded by the router adapter, falling back to the request's path when none was recorded or when a deferred resolver has nothing to report.
func ServiceAuthOrUserAuth ¶
func ServiceAuthOrUserAuth(validator ServiceKeyValidator, userAuth func(http.Handler) http.Handler) func(http.Handler) http.Handler
ServiceAuthOrUserAuth returns a middleware that authenticates a request carrying a service API key as a service, and otherwise delegates to userAuth.
func ServiceAuthRequired ¶
func ServiceAuthRequired(validator ServiceKeyValidator) func(http.Handler) http.Handler
ServiceAuthRequired returns a middleware that rejects any request not carrying a valid service API key, and publishes the caller's identity on the request context for downstream handlers.
func ServiceKeyFromRequest ¶
ServiceKeyFromRequest extracts a service API key from r, preferring the X-API-Key header and falling back to a bearer token.
func ServiceName ¶
ServiceName returns the name of the authenticated calling service.
func TraceFromRequest ¶
func TraceFromRequest(r *http.Request) *httpclient.TraceContext
TraceFromRequest returns the trace context carried by r, or nil.
func Tracing ¶
Tracing returns a middleware that adopts the caller's trace context or starts a new one, publishes it on the request context for outbound calls to continue, and echoes the identifiers on the response for correlation.
func TrackInFlight ¶
func TrackInFlight(serviceName string) func()
TrackInFlight marks a request as in flight for serviceName and returns the function that clears it.
func WithAuthType ¶
WithAuthType records how the request was authenticated.
func WithRoute ¶
WithRoute returns a context carrying the matched route pattern — the templated form such as "/api/v1/users/{id}", not the concrete path.
Routers know their own patterns and the stdlib request does not carry them, so the router adapter is responsible for recording one. Metrics and logging use it to keep label cardinality bounded; without it they fall back to the request path, which for ID-bearing routes means one label value per ID.
Example ¶
Routers know their own patterns and the standard request does not carry one, so a router adapter records it. Metrics and logging then label by template instead of by concrete path, which keeps the series count bounded.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/dobrevit/svckit/middleware"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /orders/{id}", func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(middleware.WithRoute(r.Context(), "/orders/{id}"))
fmt.Println(middleware.Route(r))
})
mux.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/orders/42", nil))
}
Output: /orders/{id}
func WithRouteFunc ¶ added in v0.1.3
WithRouteFunc returns a context whose route pattern is resolved by calling resolve, at the moment it is read.
Some routers only know the matched pattern after they have routed, which is too late for a middleware that has to attach it on the way in. chi is the common case: it puts its route context in place before matching and fills in the pattern during it, so an adapter captures the accessor here and the value arrives by the time metrics and logging ask for it.
func WithServiceIdentity ¶
func WithServiceIdentity(ctx context.Context, info *ServiceKeyInfo) context.Context
WithServiceIdentity returns a context carrying the validated service key.
Types ¶
type CORSOptions ¶
type CORSOptions struct {
// AllowedOrigins lists the origins permitted to make credentialed
// requests. The single entry "*" allows any origin.
AllowedOrigins []string
// AllowedMethods and AllowedHeaders default to DefaultCORSMethods and
// DefaultCORSHeaders when empty.
AllowedMethods []string
AllowedHeaders []string
// AllowCredentials sets Access-Control-Allow-Credentials.
AllowCredentials bool
// MaxAge caps how long a preflight result may be cached. Defaults to 12h.
MaxAge time.Duration
}
CORSOptions configures the CORS middleware.
type CheckResult ¶
type CheckResult struct {
Status string `json:"status"`
Message string `json:"message,omitempty"`
LastChecked int64 `json:"last_checked"`
Duration string `json:"duration,omitempty"`
}
CheckResult represents the result of an individual health check
type DatabaseHealthChecker ¶
type DatabaseHealthChecker struct {
// contains filtered or unexported fields
}
DatabaseHealthChecker checks database connection
func NewDatabaseHealthChecker ¶
func NewDatabaseHealthChecker(name string, ping func() error) *DatabaseHealthChecker
NewDatabaseHealthChecker creates a new database health checker
type DatabaseMetrics ¶
type DatabaseMetrics struct {
ConnectionsActive prometheus.Gauge
QueriesTotal prometheus.CounterVec
QueryDuration prometheus.HistogramVec
}
DatabaseMetrics contains database-specific metrics
func NewDatabaseMetrics ¶
func NewDatabaseMetrics(serviceName string) *DatabaseMetrics
NewDatabaseMetrics creates database-specific metrics
type EndpointType ¶
type EndpointType string
EndpointType represents different types of endpoints with different rate limits
const ( EndpointTypeAuth EndpointType = "auth" // Authentication endpoints (strict) EndpointTypeData EndpointType = "data" // Data access endpoints (moderate) EndpointTypeUpload EndpointType = "upload" // File upload endpoints (strict) EndpointTypeAdmin EndpointType = "admin" // Admin operations (moderate) EndpointTypePublic EndpointType = "public" // Public endpoints (lenient) EndpointTypeInternal EndpointType = "internal" // Internal service calls (lenient) EndpointTypeHealthy EndpointType = "health" // Health checks (no limit) )
func ClassifyEndpoint ¶
func ClassifyEndpoint(path, method string) EndpointType
ClassifyEndpoint determines the endpoint type based on the request path and method
type EventBusHealthChecker ¶
type EventBusHealthChecker struct {
// contains filtered or unexported fields
}
EventBusHealthChecker checks event bus connection
func NewEventBusHealthChecker ¶
func NewEventBusHealthChecker(name string, ping func() error) *EventBusHealthChecker
NewEventBusHealthChecker creates a new event bus health checker
type HealthCheckResponse ¶
type HealthCheckResponse struct {
Status string `json:"status"`
Service string `json:"service"`
Version string `json:"version,omitempty"`
Timestamp int64 `json:"timestamp"`
Uptime int64 `json:"uptime"`
Checks map[string]CheckResult `json:"checks,omitempty"`
}
HealthCheckResponse represents the health check response
type HealthChecker ¶
HealthChecker interface for health checks
type LoggingOptions ¶ added in v0.1.2
type LoggingOptions struct {
// ServiceName tags every line.
ServiceName string
// Logger receives the lines. A nil logger resolves to slog's default at
// call time, so a service that installs its handler after wiring its
// routes still gets the handler it installed.
Logger *slog.Logger
// Quiet decides which completed requests are logged at debug level
// instead of info. Defaults to QuietSuccessfulProbes; set it to a
// function returning false to log everything at info.
Quiet func(r *http.Request, status int) bool
}
LoggingOptions configures RequestLoggingWith.
type RateLimitConfig ¶
type RateLimitConfig struct {
RequestsPerMinute int `json:"requests_per_minute"`
BurstAllowance int `json:"burst_allowance"`
BlockDuration time.Duration `json:"block_duration"`
}
RateLimitConfig holds configuration for rate limiting
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter implements Redis-backed rate limiting
func NewRateLimiter ¶
func NewRateLimiter(redisClient *rediscluster.ClusterClient, serviceName string) *RateLimiter
NewRateLimiter creates a new rate limiter instance
func (*RateLimiter) IsAllowed ¶
func (rl *RateLimiter) IsAllowed(ctx context.Context, clientID string, endpointType EndpointType) (bool, int, error)
IsAllowed checks if the request is allowed based on rate limits
type ServiceHealthChecker ¶
type ServiceHealthChecker struct {
// contains filtered or unexported fields
}
ServiceHealthChecker checks external service connectivity
func NewServiceHealthChecker ¶
func NewServiceHealthChecker(name, url string, checker func() error) *ServiceHealthChecker
NewServiceHealthChecker creates a new service health checker
type ServiceKeyInfo ¶
type ServiceKeyInfo struct {
ServiceName string `json:"service_name"`
Scopes []string `json:"scopes"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
ServiceKeyInfo describes a validated service key.
func ServiceIdentity ¶
func ServiceIdentity(ctx context.Context) (*ServiceKeyInfo, bool)
ServiceIdentity returns the validated service key carried by ctx, if any.
type ServiceKeyValidator ¶
type ServiceKeyValidator interface {
ValidateServiceKey(apiKey string) (*ServiceKeyInfo, error)
}
ServiceKeyValidator resolves a service API key to the calling service's identity, or reports an error when the key is not valid.