SRouter stores its request-scoped values in one scontext.SRouterContext[T, U]
attached to the standard context.Context. T is the router's user ID type and
U is its user object type. Middleware and handlers must use the same type
arguments that were passed to router.NewRouter[T, U].
Use the helpers in pkg/scontext instead of reading or writing
SRouterContext fields directly. The wrapper is shared by pointer across the
middleware chain, and a handler that has timed out may briefly continue in a
goroutine while the router reads request state. The helpers synchronize access
with the wrapper's internal lock.
| Value | Write helper | Read helper |
|---|---|---|
| User ID | WithUserID |
GetUserID, GetUserIDFromRequest |
User object (*U) |
WithUser |
GetUser, GetUserFromRequest |
| Client IP | WithClientIP, WithClientInfo |
GetClientIP, GetClientIPFromRequest |
| User agent | WithUserAgent, WithClientInfo |
GetUserAgent, GetUserAgentFromRequest |
| Trace ID | WithTraceID |
GetTraceIDFromContext, GetTraceIDFromRequest |
| Build identity | WithBuildID |
GetBuildID, GetBuildIDFromRequest |
| Configuration identity | WithConfigID |
GetConfigID, GetConfigIDFromRequest |
| Database transaction | WithTransaction |
GetTransaction, GetTransactionFromRequest |
| Route template and path parameters | WithRouteInfo, SetRouteInfo |
GetRouteTemplateFromRequest, GetPathParamsFromRequest |
| Allowed CORS origin and credentials | WithCORSInfo |
GetCORSInfo, GetCORSInfoFromRequest |
| Requested CORS headers | WithCORSRequestedHeaders |
GetCORSRequestedHeaders, GetCORSRequestedHeadersFromRequest |
| Generic-handler error | WithHandlerError |
GetHandlerError, GetHandlerErrorFromRequest |
| Application boolean flag | WithFlag |
GetFlag, GetFlagFromRequest |
Most getters return (value, ok) so an unset value can be distinguished from
its zero value. The trace-ID getters instead return an empty string when no
trace ID is set. WithTraceID preserves an existing ID rather than overwriting
one propagated by an upstream service.
Applications may configure RouterDependencies.BuildID and
RouterDependencies.ConfigID to install opaque, log-safe runtime identities.
SRouter samples each non-nil provider once at the beginning of every request,
before CORS, routing, and middleware. Empty results remain unset; a non-empty
local result replaces an inherited identity. Providers must be concurrency-safe,
fast, and non-panicking.
These identities are not propagated through request or response headers.
Non-HTTP work, such as background workers, can install already-sampled values
with WithBuildID and WithConfigID.
The router populates client information and, after a route match, its route
template and path parameters. When CORS is configured, CORS information is
stored even when the request has no Origin or the origin is denied; an empty
stored origin represents that outcome. When a typed handler completes through
the normal chain, its returned error is recorded before the remaining
middleware unwinds. A handler that continues after the timeout stage returns
may record its error later.
Values returned by the helpers can themselves be references. In particular,
the user is a *U, the transaction is an interface, and path parameters are a
slice. Treat those referenced values as shared unless your application makes
its own copy.
Each With* helper returns the context to propagate. This matters when the
request did not already contain an SRouter context and the helper had to create
one.
func TagAdmin[UserID comparable, User any](
isAdmin func(*http.Request) bool,
) common.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := scontext.WithFlag[UserID, User](
r.Context(), "is_admin", isAdmin(r),
)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}Middleware that needs to inspect state after the handler should retain the derived request rather than reading the original request's context:
ctx := scontext.WithFlag[string, User](r.Context(), "audited", true)
nextRequest := r.WithContext(ctx)
next.ServeHTTP(w, nextRequest)
handlerErr, failed := scontext.GetHandlerErrorFromRequest[string, User](nextRequest)
_ = handlerErr
_ = failedfunc accountHandler(w http.ResponseWriter, r *http.Request) {
userID, authenticated := scontext.GetUserIDFromRequest[string, User](r)
if !authenticated {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
user, hasUser := scontext.GetUserFromRequest[string, User](r)
clientIP, _ := scontext.GetClientIPFromRequest[string, User](r)
routeTemplate, _ := scontext.GetRouteTemplateFromRequest(r)
_, _, _ = userID, user, hasUser
_, _ = clientIP, routeTemplate
}Transactions stored in the context implement scontext.DatabaseTransaction:
type DatabaseTransaction interface {
Commit() error
Rollback() error
SavePoint(name string) error
RollbackTo(name string) error
GetDB() *gorm.DB
}GORM's *gorm.DB does not implement this interface directly because its
transaction methods return *gorm.DB. Wrap it with
middleware.NewGormTransactionWrapper before storing it:
tx := db.Begin()
if tx.Error != nil {
return tx.Error
}
ctx := scontext.WithTransaction[string, User](
r.Context(),
middleware.NewGormTransactionWrapper(tx),
)
next.ServeHTTP(w, r.WithContext(ctx))Use Commit, Rollback, SavePoint, and RollbackTo through the interface.
Call GetDB() when handler code needs the underlying GORM transaction.
CopySRouterContext[T, U](dst, src) attaches a new wrapper containing the
source values to dst. It preserves dst's cancellation and deadline chain.
If src has no SRouter context, it returns dst unchanged.
CopySRouterContextOverlay[T, U](dst, src) performs the same replacement only
when both contexts already contain an SRouter context. It is a no-op when either
wrapper is absent. It replaces the destination values; it does not merge them.
Both functions create an independent wrapper and copy the mutable Flags map
and PathParams slice. Other fields are assigned normally. Consequently,
reference-bearing values—including User, Transaction, HandlerError, and
any pointer-bearing user ID—still refer to the same underlying objects. These
functions are therefore not recursive deep-copy operations.