Skip to content

Latest commit

 

History

History
162 lines (132 loc) · 6.52 KB

File metadata and controls

162 lines (132 loc) · 6.52 KB

Context management

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.

Stored values

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.

Writing values in middleware

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
_ = failed

Reading values in a handler

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

Database transactions

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.

Copying SRouter context values

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.