router

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package router provides a chi-based HTTP router that implements kernel/cell.RouteMux with default middleware and automatic health/metrics endpoint registration.

Package router provides an HTTP router that implements kernel/cell.RouteMux with default observability middleware. Each Router instance wraps a single stdlib *http.ServeMux for ONE physical listener. Bootstrap builds one Router per declared listener (primary / internal / health) and applies the listener's default Policy before any routes are registered.

ref: net/http.ServeMux (Go 1.22+) — method+pattern routing, automatic 405, automatic HEAD-from-GET, r.PathValue for path params. Adopted: stdlib ServeMux as the underlying multiplexer, one per listener. Wrapped behind kernel/cell.RouteMux interface so Cells remain decoupled from the multiplexer choice.

ref: go-kratos/kratos transport/http/server.go — per-server middleware Adopted: policy applied at server build time; observability baked in.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Middleware

type Middleware = func(http.Handler) http.Handler

Middleware is the standard net/http middleware shape used throughout the router and its adapters.

type Option

type Option func(*Router)

Option configures a Router.

func AdminPrefixIsolationResponder

func AdminPrefixIsolationResponder() Option

AdminPrefixIsolationResponder returns a router.Option that 404s any request whose path starts with /admin/v1 (or equals the bare path) BEFORE any auth or policy middleware runs. Bootstrap installs this on the primary listener (the admin symmetric counterpart of InternalPrefixIsolationResponder, #1505) so the primary listener never reveals that /admin/v1/* operator endpoints exist — an undeclared admin-path probe to the public port 404s like an unknown path, rather than 401-ing through the JWT chain. The real admin endpoints live on the loopback AdminListener; this responder is the public-port isolation contract.

func InternalPrefixIsolationResponder

func InternalPrefixIsolationResponder() Option

InternalPrefixIsolationResponder returns a router.Option that 404s any request whose path starts with /internal/v1 (or equals the bare path) BEFORE any auth or policy middleware runs. Bootstrap installs this on the primary listener via WithEarlyResponder so the physical-isolation contract — primary listener never reveals that /internal/v1/* routes exist — does not depend on a JWT public-matcher exemption.

PR-258 RES-5 narrowing: replaces the prior listener-mux Handle 404 + WithPublicPathPrefix("/internal/v1/") + frameworkPrimaryWhitelist triple- mechanism. The new model has a single surface: the predicate.

See docs/ops/listener-topology.md for the deployment topology, threat boundaries, and single-listener migration guide that frame this isolation rule.

func WithAuthMetrics

func WithAuthMetrics(m *auth.AuthMetrics) Option

WithAuthMetrics sets the AuthMetrics instance used by AuthMiddleware when wired via WithAuthMiddleware.

func WithAuthMiddleware

func WithAuthMiddleware(verifier kauth.IntentTokenVerifier) Option

WithAuthMiddleware enables authentication middleware with an explicitly injected verifier. The middleware is placed in the mux chain after any rate-limiter/circuit-breaker and before BodyLimit. Public endpoints declared via auth.Mount with Public:true inside cell RouteGroups bypass JWT verification; FinalizeAuth compiles them into the router's auth predicates.

In the per-listener model this option is most commonly used for the PrimaryListener router. InternalListener routers use auth.NewAuthServiceToken or auth.AuthMTLS{} at the listener level, not this option.

ref: go-kratos/kratos — auth middleware at service level with selector-based bypass ref: go-zero — per-route WithJwt() opt-in auth

func WithBodyLimit

func WithBodyLimit(maxBytes int64) Option

WithBodyLimit overrides the default request body size limit.

func WithCellIDClosedSet

func WithCellIDClosedSet(cellIDs []string) Option

WithCellIDClosedSet injects the assembly's closed cell-id set (the assembly.yaml cells list) so the Metrics / BodyLimit middleware can validate the request's cell label against it at the metric write point (M12b #1093): an out-of-set cell id degrades to metrics.RuntimeCellSentinel instead of polluting the platform SLO series, via the sealed metrics.ResolveCellLabel funnel.

This is an accumulating builder-style option, not a wiring dependency: an empty/nil set is a legitimate "no assembly to filter against" state (standalone router / tests) under which every cell degrades to the sentinel. Bootstrap always supplies s.asm.CellIDs() in production. A copy is taken so a later mutation of the caller's slice cannot widen the set after build.

func WithCircuitBreaker

func WithCircuitBreaker(cb middleware.Allower) Option

WithCircuitBreaker enables a circuit breaker in the default middleware chain. A nil cb is rejected by NewForListener so the circuit breaker is never silently absent.

ref: go-zero — circuit breaker as default middleware when configured ref: go-kit/kit circuitbreaker — middleware wrapping pattern ref: kubernetes/kubernetes apiserver — option fail-fast at startup

func WithClientErrorLogSampling

func WithClientErrorLogSampling(every int) Option

WithClientErrorLogSampling sets the deterministic sample rate for 4xx logs on contract-bound requests. Values less than one log every 4xx.

func WithDefaultMiddleware

func WithDefaultMiddleware(mws ...func(http.Handler) http.Handler) Option

WithDefaultMiddleware appends middleware functions to the router's default middleware chain. These are installed AFTER the early-responder layer and BEFORE the per-router protections (rate-limiter, circuit-breaker, auth).

Bootstrap uses this to install the listener-level auth middleware derived from the ListenerAuth chain (e.g. mTLS peer-cert check, ServiceToken HMAC guard). Multiple calls append in order.

func WithEarlyResponder

func WithEarlyResponder(predicate func(*http.Request) bool, handler http.HandlerFunc) Option

WithEarlyResponder registers a predicate-driven middleware that runs before the policy layer (auth, rate limit, etc.). When the predicate matches, handler writes the response and the chain short-circuits.

Intended for framework-owned isolation contracts that should NOT depend on a public-matcher exemption. The canonical example is the primary listener's /internal/v1/* 404 isolation: by deciding the response BEFORE auth runs, we avoid having to add the prefix to the JWT public-bypass matcher (which would also bypass any cell route mistakenly registered under that prefix). PR-258 RES-5 narrowing.

Multiple WithEarlyResponder calls accumulate in declaration order; the first matching predicate wins.

func WithIdempotency

func WithIdempotency(store idemhttp.Store) Option

WithIdempotency enables HTTP idempotency middleware for mutating methods (POST/PUT/PATCH/DELETE). The middleware is placed in the chain after BodyLimit, so request bodies are already bounded before idempotency key derivation. When wired together with WithAuthMiddleware, idempotency runs AFTER auth so the authenticated Principal is available for namespace composition.

Both bare-nil and typed-nil (non-nil interface holding a nil pointer) are rejected by NewForListener so idempotency is never silently absent.

func WithIdempotencyMetrics

func WithIdempotencyMetrics(obs idemhttp.MetricsObserver) Option

WithIdempotencyMetrics installs an optional MetricsObserver that the idempotency middleware calls once per terminal decision to record idempotency_requests_total{cell,state}. This is an OPTIONAL setter (not a fail-fast wiring option): a nil observer simply disables metric emission — idempotency correctness never depends on it. Bootstrap auto-wires the concrete runtime/observability/metrics.IdempotencyCollector here when a real metrics Provider is configured; without a Provider the option is never applied and the middleware runs unmetered. The observer is only consumed when WithIdempotency is also wired (buildMux constructs the middleware only then).

func WithMetricsCollector

func WithMetricsCollector(c metrics.Collector) Option

WithMetricsCollector adds the metrics middleware using the given Collector. To also serve a /metrics endpoint, pass the handler to the RouteGroups mechanism.

func WithPolicyCoverageWhitelist

func WithPolicyCoverageWhitelist(patterns []string) Option

WithPolicyCoverageWhitelist sets route patterns exempt from policy coverage verification.

ref: kubernetes/apiserver — structural injection guarantees authorizer presence; GoCell's startup-time verification achieves the same guarantee.

func WithPublicPathPrefix

func WithPublicPathPrefix(prefix string) Option

WithPublicPathPrefix pre-seeds the router's public-endpoint matcher so that any request whose URL path begins with prefix bypasses JWT authentication. Multiple calls OR-merge the predicates. The seed is applied before FinalizeAuth compiles the per-route declarations, so the merged matcher includes both the prefix exemption and any auth.Mount(Public:true) routes.

Use this for framework-owned path prefixes that must be exempt from auth but are not declared via auth.Mount (e.g. the /internal/v1/* 404 isolation handler on the primary listener).

F6 round-3: matches both the prefix form (".../path/") and the exact bare path (".../path") so the canonical isolation 404 fires for both /internal/v1 and /internal/v1/foo. Without the bare-path branch the pre-prefix path was challenged by JWT (401) instead of reaching the 404 isolation handler — leaking the existence of the internal subtree.

func WithRateLimiter

func WithRateLimiter(rl middleware.RateLimiter) Option

WithRateLimiter enables per-IP rate limiting in the default middleware chain. When provided, the rate limiter is placed after observability and before auth.

Both bare-nil and typed-nil (non-nil interface holding a nil pointer) are rejected by NewForListener so the rate limiter is never silently absent.

ref: go-zero — rate limiting as default middleware when configured

func WithRequestIDOptions

func WithRequestIDOptions(opts ...middleware.RequestIDOption) Option

WithRequestIDOptions passes additional RequestIDOption values to the RequestID middleware.

func WithSecurityHeadersOptions

func WithSecurityHeadersOptions(opts ...middleware.SecurityHeadersOption) Option

WithSecurityHeadersOptions passes additional SecurityHeadersOption values to the SecurityHeaders middleware.

ref: unrolled/secure — configurable HSTS directives via struct fields

func WithSuppressNoAuthVerifierWarn

func WithSuppressNoAuthVerifierWarn() Option

WithSuppressNoAuthVerifierWarn silences the FinalizeAuth slog.Warn that fires when auth route metas are declared on a router with no AuthMiddleware installed.

Intended for routers that intentionally serve auth-declared routes without a JWT verifier — typically the HealthListener (whose framework probes use auth.Mount with Public:true) and the InternalListener (which gates traffic with auth.AuthMTLS{} or auth.NewAuthServiceToken instead of JWT). Without this opt-out the router emits a Warn at every production startup, drowning operators in alert noise. R2-11.

func WithTracer

func WithTracer(t wrapper.Tracer) Option

WithTracer enables distributed tracing middleware using the given Tracer. When provided, each request gets a trace span with trace_id and span_id propagated through context. Inbound W3C `traceparent` headers are extracted before span creation, with B3 used only as a fallback. The tracing middleware is placed after Recorder and before AccessLog so trace IDs appear in access logs.

ref: go-zero — observability wired by default when configured ref: otelchi — chi middleware for OpenTelemetry trace propagation

func WithTracingOptions

func WithTracingOptions(opts ...middleware.TracingOption) Option

WithTracingOptions passes additional TracingOption values to the Tracing middleware.

func WithTrustedProxies

func WithTrustedProxies(proxies []string) Option

WithTrustedProxies configures the set of trusted proxy IPs/CIDRs for X-Forwarded-For header processing.

ref: gin-gonic/gin — SetTrustedProxies([]string) with CIDR support

type Router

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

Router wraps a single *http.ServeMux root for ONE physical listener. The observability middleware chain is baked in at construction time, and the listener's default Policy is applied as an inner layer before any cell routes are registered.

Bootstrap creates one Router per declared listener (primary/internal/health) via NewForListener. The old shared-root multiplexer design has been replaced by this per-listener model.

ref: go-kratos/kratos transport/http/server.go — per-server middleware ref: net/http.ServeMux — one ServeMux root per listener; method+pattern

routing (Go 1.22+).

func New

func New(clk clock.Clock, opts ...Option) (*Router, error)

New creates a Router with default middleware and optional configuration. It returns an error when configuration is invalid.

The resulting Router serves the zero-value ListenerRef (no listener affinity). For production use call NewForListener instead.

ref: gin-gonic/gin — SetTrustedProxies returns error at config time ref: uber-go/fx — startup failures return error, trigger rollback

func NewForListener

func NewForListener(clk clock.Clock, ref kcell.ListenerRef, opts ...Option) (*Router, error)

NewForListener builds a Router for a specific listener. Listener-level authentication middleware (mTLS, ServiceToken, etc.) is injected by Bootstrap via WithDefaultMiddleware(mws...) after applyListenerAuthChain converts the ListenerAuth chain; there is no longer a separate defaultPolicy parameter. JWT auth is wired via WithAuthMiddleware (a separate Option) so the router-aware Public/PasswordResetExempt matchers are available.

clk is a mandatory positional parameter; passing a nil clock panics at construction time via clock.MustHaveClock (programmer error).

The middleware chain is:

ListenerContext → RequestID → RealIP → Recorder → CellAttribution → [Tracing] → AccessLog → [Metrics]
→ Recovery → SecurityHeaders → [earlyResponders] → [defaultMiddleware]
→ [RateLimit] → [CircuitBreaker] → [Auth] → BodyLimit → [Idempotency] → handlers

ref: go-kratos/kratos app.go WithServer + errgroup (adopted) ref: net/http.ServeMux (one ServeMux per listener) ref: kubernetes/kubernetes apiserver/server/genericapiserver.go (rejected single-listener)

func (*Router) DeclareAuthMeta

func (r *Router) DeclareAuthMeta(m kcell.AuthRouteMeta) error

DeclareAuthMeta implements cell.AuthRouteDeclarer. It accumulates auth route metadata forwarded by auth.Mount during RouteGroup.Register. FinalizeAuth compiles the accumulated metas into matchers that the AuthMiddleware reads via lazy closures installed in buildAuthOpts.

Returns an error if called after FinalizeAuth — all declarations must precede the compile step.

func (*Router) DeclareHTTPContract

func (r *Router) DeclareHTTPContract(spec contractspec.ContractSpec) error

DeclareHTTPContract implements cell.HTTPContractDeclarer. It stores the route contract metadata separately from auth metadata so Tracing can tag spans for requests rejected before reaching wrapper.HTTPHandler.

func (*Router) DeclaredAuthMetas

func (r *Router) DeclaredAuthMetas() []kcell.AuthRouteMeta

DeclaredAuthMetas returns a copy of all AuthRouteMeta entries accumulated.

func (*Router) FinalizeAuth

func (r *Router) FinalizeAuth() error

FinalizeAuth compiles all accumulated AuthRouteMeta declarations into matchers used by the auth middleware. Bootstrap calls this after all cells have completed route registration but before the HTTP listener starts.

Internal-route affinity is derived from path prefix via AuthRouteMeta.IsInternal(); /internal/v1/* routes on non-InternalListener routers fail fast at startup. Zero-ref routers (NewE / unit tests) skip the listener-identity check.

Returns error (not panic) so Bootstrap can perform a clean rollback:

  • "router: FinalizeAuth called twice"
  • "router: duplicate auth declaration METHOD /path"

It is safe to call FinalizeAuth with an empty declaredAuthMetas slice; the policy coverage check still runs to catch routes registered without any auth.Mount call.

func (*Router) Group

func (r *Router) Group(fn func(kcell.RouteMux))

Group creates a sub-scope, implementing cell.RouteMux. The returned adapter shares the same underlying ServeMux as the parent. Group is a structural helper for clustering related route registrations; it does not introduce its own middleware scope (use With for that).

func (*Router) Handle

func (r *Router) Handle(pattern string, handler http.Handler)

Handle registers a handler for the given pattern, implementing cell.RouteMux. pattern follows the stdlib ServeMux 1.22 form ("METHOD /path/{param}" or "/path"). The handler is registered directly on the underlying ServeMux without any extra middleware wrap; root-level middleware (observability, auth, body-limit, etc.) is composed by Router.Handler.

Each registration is mirrored into routePatterns so policy coverage and route-pattern resolution see the same routes that ServeMux dispatches to. Duplicate registrations are tracked separately (muxHandlers) so the second call drops at the mux layer — FinalizeAuth's structured error is the canonical user-visible signal for duplicate auth.Mount.

func (*Router) Handler

func (r *Router) Handler() http.Handler

Handler returns the http.Handler for this listener's router. This is the handler to pass to http.Server. Unlike the legacy PublicHandler()/ InternalHandler(), each listener now has exactly one Handler().

The composition (outer → inner) is:

r.patternRecorderMiddleware → r.middlewares (in declaration order) →
  patternRecordingMux → *http.ServeMux → leaf handler

r.patternRecorderMiddleware installs the *patternRecorder and injects the router's route resolver so all observability layers see a consistent route label even on short-circuit reject paths (auth, rate limit, 405). patternRecordingMux fills the recorder by asking ServeMux.Handler for the matched pattern before dispatching the leaf handler.

The handler is built eagerly by composeHandler at the end of buildMux (called from NewForListener), so Handler() is a plain getter and safe to call concurrently after construction. Handler returns the listener's composed http.Handler. Built once at construction by composeHandler; safe to call concurrently afterwards.

func (*Router) Mount

func (r *Router) Mount(prefix string, handler http.Handler)

Mount attaches an http.Handler under the given prefix. Stdlib ServeMux requires sub-tree patterns to end in "/"; the trailing slash is added implicitly. The handler sees the request path with the prefix stripped.

Both the bare prefix ("/api") and the sub-tree ("/api/...") are registered so requests that hit the mount root without a trailing slash still reach the inner handler's "/" registration instead of stdlib's 301 redirect.

func (*Router) MountRouteGroup

func (r *Router) MountRouteGroup(rg kcell.RouteGroup) error

MountRouteGroup mounts a cell RouteGroup and records its HTTP namespace ownership for root-level observability. Cell ownership is resolved before protection middleware, so auth/rate-limit/circuit-breaker/body-limit rejects and ServeMux 405 responses all receive the same cell label as successful handler executions.

func (*Router) Route

func (r *Router) Route(pattern string, fn func(kcell.RouteMux))

Route mounts a sub-router under the given prefix. The adapter carries both the prefix and a reference to the Router-rooted declarer so that auth.Mount called on a nested sub-mux composes the mount prefix with the declared path and forwards AuthRouteMeta to the top-level Router.

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP delegates to the router's composed handler, making Router drop-in compatible with http.Handler. If auth route metadata was declared but FinalizeAuth was not called, ServeHTTP fails closed with 500 instead of serving routes with incomplete auth state.

func (*Router) With

func (r *Router) With(mw ...func(http.Handler) http.Handler) kcell.RouteMux

With returns a new RouteMux that applies the given middleware to routes registered through it, without modifying the receiver. Each subsequent Handle on the returned adapter wraps the handler with the captured chain before registering on the shared ServeMux.

Jump to

Keyboard shortcuts

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