middleware

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package middleware provides net/http middleware for sqi-server.

Middleware in this package is router-agnostic: each function returns a standard func(http.Handler) http.Handler that can be composed directly or mounted via any router (chi, gorilla/mux, net/http ServeMux, etc.).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func APIVersion

func APIVersion(version string) func(http.Handler) http.Handler

APIVersion returns middleware that sets the X-API-Version response header on every response so clients can verify which version of the sqi API they are talking to without inspecting the URL.

The header is added to the response map before the inner handler runs, so it is present even on error responses. Go's net/http flushes all headers that were Set/Add before the first Write or WriteHeader call, which is why setting headers prior to calling next.ServeHTTP is the correct pattern here.

Usage (applied to a chi sub-router):

r.Route("/api/v1", func(api chi.Router) {
    api.Use(middleware.APIVersion("1"))
    // … routes …
})

func Deprecated

func Deprecated(declaredAt, sunsetDate time.Time, link string) func(http.Handler) http.Handler

Deprecated returns middleware that marks an endpoint as deprecated by injecting the standard HTTP deprecation headers defined in RFC 8594.

Headers set:

  • Deprecation: <RFC 7231 HTTP-date> — when the deprecation was declared. Use time.Time zero value to emit "true" instead of a date, which is valid per the RFC when the declaration date is not meaningful.
  • Sunset: <RFC 7231 HTTP-date> — when the endpoint will be removed. Omitted when sunsetDate is the zero value.
  • Link: <link>; rel="deprecation" — URL of documentation describing the deprecation and migration path. Omitted when link is empty. link must be a valid absolute URL; a syntactically invalid value (e.g. one containing an unescaped '>') will produce a malformed Link header.

Wrap individual handlers (not an entire route group) with Deprecated so that only the specific endpoints that are being retired carry these headers.

Both chi usage patterns work:

// Pattern 1: inline wrap
api.Get("/v1/old-endpoint", middleware.Deprecated(
    time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC),
    time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC),
    "https://docs.sqi.dev/api/deprecations#old-endpoint",
)(oldHandler))

// Pattern 2: chi.With (preferred when combining multiple per-route middleware)
api.With(middleware.Deprecated(
    time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC),
    time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC),
    "https://docs.sqi.dev/api/deprecations#old-endpoint",
)).Get("/v1/old-endpoint", oldHandler)

func RateLimit

func RateLimit(cfg RateLimitConfig, logger *slog.Logger) func(http.Handler) http.Handler

RateLimit returns middleware that enforces a per-IP token-bucket rate limit. Clients that exceed the limit receive HTTP 429 Too Many Requests with an RFC 7807 problem-details body (matching every other error on the API surface) and a delta-seconds Retry-After header derived from the bucket's refill rate.

Each remote IP maintains its own bucket; buckets are allocated lazily on first request. Rejected requests return their reserved token (best-effort under concurrent rejections — see Reservation.Cancel), so being rejected does not push a client's recovery time out further.

Phase 1 limitations (both deferred to Phase 3):

  • No LRU eviction: the limiter map grows unbounded. This is acceptable for Phase 1's trusted local deployments where the client IP space is small. Phase 3 will introduce an LRU cache (e.g. golang.org/x/time/rate + github.com/hashicorp/golang-lru) sized to expected concurrent client count.

  • RemoteAddr only: when a reverse proxy is in front, all clients share the proxy's IP and effectively one bucket. Phase 3 will honor X-Forwarded-For / X-Real-IP after the trusted-proxy list is configured alongside the auth layer.

Usage (applied to the /api/v1 sub-router):

api.Use(middleware.RateLimit(middleware.RateLimitConfig{
    RequestsPerSecond: 20,
    Burst:             40,
}, logger))

func RequestIDFromContext

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext returns the request ID attached to ctx by RequestLogger, or an empty string if ctx carries no request ID.

func RequestLogger

func RequestLogger(logger *slog.Logger) func(http.Handler) http.Handler

RequestLogger returns middleware that, for every request:

  1. Reads the X-Request-ID header; if absent, generates a random 16-char hex ID using crypto/rand.
  2. Stores the request ID in the request context (retrieve with RequestIDFromContext).
  3. Echoes the request ID in the X-Request-ID response header.
  4. Wraps the http.ResponseWriter to capture the status code.
  5. After the handler returns, logs method, path, status code, duration, and request ID at Info level using the provided *slog.Logger.

WebSocket upgrades (HTTP 101) are handled transparently: the wrapped writer implements http.Hijacker so the upgrade handshake proceeds normally, and the middleware logs the 101 status after the connection is hijacked.

func RequestMetrics

func RequestMetrics(m *metrics.Metrics) func(http.Handler) http.Handler

RequestMetrics returns middleware that records Prometheus metrics for every HTTP request:

  • sqi_http_requests_total — counter incremented after the handler returns, labeled by method, path, and status code.
  • sqi_http_request_duration_seconds — histogram observed after the handler returns, labeled by method and path.

The "path" label is currently set to r.URL.Path. Once REST endpoints with path parameters are added, replace this with chi's route pattern via chi.RouteContext(r.Context()).RoutePattern() to prevent high cardinality from parameterised segments such as /api/v1/jobs/{id}.

func WriteProblem

func WriteProblem(w http.ResponseWriter, r *http.Request, status int, detail string)

WriteProblem writes an RFC 7807 problem-details response. It sets Content-Type: application/problem+json and the given HTTP status, then encodes a problemDetail body. detail should be a short, user-facing explanation of what went wrong.

Types

type RateLimitConfig

type RateLimitConfig struct {
	// RequestsPerSecond is the steady-state refill rate for each IP bucket.
	// Defaults to 20 when zero.
	RequestsPerSecond rate.Limit

	// Burst is the maximum number of requests allowed in a single instant.
	// Defaults to 40 when zero.
	Burst int
}

RateLimitConfig controls the per-IP token-bucket parameters.

Jump to

Keyboard shortcuts

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