middleware

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

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

Examples

Constants

View Source
const (
	StatusHealthy   = "healthy"
	StatusUnhealthy = "unhealthy"
)

StatusHealthy and StatusUnhealthy are the values a HealthChecker reports.

View Source
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.

View Source
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.

View Source
const ScopeAll = "all"

ScopeAll is the wildcard scope that satisfies any RequireScope check.

Variables

View Source
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.

DefaultCORSMethods is the method allowlist applied when CORSOptions leaves AllowedMethods empty.

View Source
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

func CORSWithOrigins(allowedOrigins []string) func(http.Handler) http.Handler

CORSWithOrigins returns a credentialed CORS middleware restricted to allowedOrigins, using the default method and header allowlists.

func ClientIP

func ClientIP(r *http.Request) string

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

func ClientIdentifier(r *http.Request) string

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

func HasScope(ctx context.Context, scope string) bool

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 IsServiceAuthenticated

func IsServiceAuthenticated(ctx context.Context) bool

IsServiceAuthenticated reports whether ctx was authenticated as a service.

func Metrics

func Metrics(serviceName string) func(http.Handler) http.Handler

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

func MetricsHandler() http.Handler

MetricsHandler serves the Prometheus exposition format.

func ObserveRequest

func ObserveRequest(serviceName, method, route string, status int, d time.Duration)

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 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

func RequestLogging(serviceName string) func(http.Handler) http.Handler

RequestLogging returns a middleware that logs one line per completed request through slog's default logger.

func RequestLoggingTo

func RequestLoggingTo(logger *slog.Logger, serviceName string) func(http.Handler) http.Handler

RequestLoggingTo returns a middleware that logs one line per completed request — method, route, status, size, duration and client address, tagged with serviceName — through logger. 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.

func RequireServiceScope

func RequireServiceScope(requiredScope string) func(http.Handler) http.Handler

RequireServiceScope returns a middleware that rejects an authenticated service lacking requiredScope. It must run after ServiceAuthRequired.

func Route

func Route(r *http.Request) string

Route returns the matched route pattern recorded by the router adapter, falling back to the request's path when none was recorded.

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

func ServiceKeyFromRequest(r *http.Request) string

ServiceKeyFromRequest extracts a service API key from r, preferring the X-API-Key header and falling back to a bearer token.

func ServiceName

func ServiceName(ctx context.Context) (string, bool)

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

func Tracing() func(http.Handler) http.Handler

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 UserID

func UserID(ctx context.Context) (string, bool)

UserID returns the authenticated end user's ID carried by ctx, if any.

func WithAuthType

func WithAuthType(ctx context.Context, at AuthType) context.Context

WithAuthType records how the request was authenticated.

func WithRoute

func WithRoute(ctx context.Context, pattern string) context.Context

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 WithServiceIdentity

func WithServiceIdentity(ctx context.Context, info *ServiceKeyInfo) context.Context

WithServiceIdentity returns a context carrying the validated service key.

func WithUserID

func WithUserID(ctx context.Context, id string) context.Context

WithUserID returns a context carrying the authenticated end user's ID.

Types

type AuthType

type AuthType string

AuthType names how a request was authenticated.

const (
	// AuthTypeService marks a request authenticated by a service API key.
	AuthTypeService AuthType = "service"
	// AuthTypeUser marks a request authenticated as an end user.
	AuthTypeUser AuthType = "user"
)

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

func (*DatabaseHealthChecker) Check

func (d *DatabaseHealthChecker) Check() (string, string, string)

Check implements HealthChecker

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

func (*EventBusHealthChecker) Check

func (e *EventBusHealthChecker) Check() (string, string, string)

Check implements HealthChecker

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

type HealthChecker interface {
	Check() (name string, status string, message string)
}

HealthChecker interface for health checks

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

func (*ServiceHealthChecker) Check

func (s *ServiceHealthChecker) Check() (string, string, string)

Check implements HealthChecker

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.

Jump to

Keyboard shortcuts

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