Interceptors
Powerful request gatekeeping and enrichment with high-performance interceptors
Interceptors in Forge provide a lightweight, high-performance way to inspect, block, or enrich requests before they reach your handler. Unlike middleware, interceptors are designed for precise control flow logic like authentication, authorization, feature flagging, and tenant isolation without the overhead of wrapping the entire handler chain.
Interceptors vs Middleware
| Feature | Middleware | Interceptors |
|---|---|---|
| Execution | Wraps handler (before/after) | Runs before handler only |
| Control Flow | Calls next(ctx) | Returns Allow() or Block() |
| Primary Use | Cross-cutting concerns (Logging, CORS, Recovery) | Gatekeeping & Enrichment (Auth, RBAC, Rate Limits) |
| Complexity | Higher (manages chain) | Lower (single decision point) |
| Skipping | Difficult (usually by path matching) | Easy (by name) |
| Parallelism | Sequential only | Supports parallel execution |
When to use which? Use Middleware for things that affect the entire request lifecycle or response (Logging, CORS, Panic Recovery). Use Interceptors for decision logic that should happen before the handler (Auth checks, Feature flags, Data loading).
Basic Usage
Interceptors are simple functions that return an InterceptorResult.
import "github.com/xraph/forge"
// Define a simple interceptor
var RequireAdmin = forge.NewInterceptor("require-admin", func(ctx forge.Context, route forge.RouteInfo) forge.InterceptorResult {
role := ctx.Get("user.role")
if role != "admin" {
return forge.Block(forge.Forbidden("admin access required"))
}
return forge.Allow()
})
// Use it in a route
app.Router().GET("/admin/dashboard", dashboardHandler,
forge.WithInterceptor(RequireAdmin),
)The Interceptor Result
Interceptors communicate their decision via InterceptorResult:
// Allow the request to proceed
return forge.Allow()
// Block the request with an error
return forge.Block(forge.Unauthorized("login required"))
// Allow and enrich the context with values
return forge.AllowWithValues(map[string]any{
"user_id": "123",
"scopes": []string{"read", "write"},
})
// Block but return values (useful for rate limit headers on failure)
return forge.BlockWithValues(
forge.TooManyRequests("limit exceeded"),
map[string]any{"Retry-After": "60"},
)Group & Route Interceptors
Interceptors can be applied at the group level or route level. They execute in the following order:
- Group Interceptors (inherited)
- Route Interceptors
// Group with shared interceptors
adminAPI := app.Router().Group("/admin",
forge.WithGroupInterceptor(RequireAuth(), RequireAdmin),
)
// Inherits Auth + Admin checks
adminAPI.GET("/users", listUsers)
// Inherits Auth + Admin, adds Audit check
adminAPI.DELETE("/users/:id", deleteUser,
forge.WithInterceptor(AuditLog),
)Skipping Interceptors
One of the most powerful features of interceptors is the ability to skip them by name. This allows you to apply strict rules at the group level but make exceptions for specific routes.
// Group requires authentication for everything
api := app.Router().Group("/api",
forge.WithGroupInterceptor(RequireAuth()),
)
// This route inherits RequireAuth
api.GET("/profile", getProfile)
// This route SKIPS RequireAuth (e.g., login endpoint)
api.POST("/login", loginHandler,
forge.WithSkipInterceptor("require-auth"),
)Parallel Execution
Interceptors can run in parallel to improve performance, especially when multiple checks involve I/O (like DB calls or external services).
Parallel (All Must Pass)
Executes all interceptors concurrently. Fails if any block.
app.Router().GET("/dashboard", handler,
forge.WithInterceptor(
forge.Parallel(
EnrichUser(loadUserFromDB), // I/O operation
EnrichPreferences(loadPrefsFromDB), // I/O operation
EnrichNotifications(loadNotifsFromAPI), // External call
),
),
)ParallelAny (First Success Wins)
Executes concurrently and proceeds as soon as any interceptor allows. Useful for multiple auth methods.
app.Router().GET("/data", handler,
forge.WithInterceptor(
forge.ParallelAny(
RequireJWTAuth(),
RequireAPIKey(),
RequireOAuth2(),
),
),
)Combinators
Forge provides powerful combinators to build complex logic from simple, reusable interceptors.
| Combinator | Description |
|---|---|
And(a, b) | Both must pass (Sequential). Alias: ChainInterceptors |
Or(a, b) | Either can pass (Sequential) |
Not(a) | Inverts the result (Block ↔ Allow) |
When(cond, a) | Runs a only if condition is true |
Unless(cond, a) | Skips a if condition is true |
IfMetadata(k, v, a) | Runs a if route metadata matches |
IfTag(t, a) | Runs a if route has tag |
Examples
// Complex Logic
forge.WithInterceptor(
// Run rate limiting only for "public" routes
forge.IfTag("public", RateLimit),
// Skip auth if X-Internal-Token header is present
forge.Unless(
func(ctx forge.Context, _ forge.RouteInfo) bool {
return ctx.Header("X-Internal-Token") == "secret"
},
RequireAuth(),
),
)Built-In Interceptors Library
Forge comes with a standard library of common interceptors in the forge package.
Authentication & Authorization
RequireAuth()- Checks if user/auth context existsRequireAuthProvider(name)- Checksauth.providervalueRequireScopes("read", "write")- Checks for required scopesRequireRole("admin")- Checks for user roleRequireAnyScope("read", "admin")- Allows if any scope matchesRequireAllRoles("admin", "editor")- Checks if user has ALL roles
Validation
RequireHeader("X-API-Key")- Ensures header presenceRequireContentType("application/json")- Validates content type
Access Control
AllowIPs("127.0.0.1")- IP WhitelistingDenyIPs("1.2.3.4")- IP BlacklistingTimeWindow(9, 17, location)- Restricts access to business hoursMaintenance(checker)- Blocks requests when checker returns true
Feature Flags & Tenants
FeatureFlag("beta-features", checker)- Checks feature flag using functionFeatureFlagFromContext("beta-features")- Checks feature flag from context valueTenantIsolation("tenantId")- Ensures user can only access their tenant's data
Data Enrichment
Enrich("key", loader)- Loads data and adds to contextEnrichUser(loader)- Specialized loader for user dataEnrichFromService[T]("svcName", loader)- Resolves service from DI container and uses it to load data
Metadata & Tags
RequireMetadata("key", "value")- Enforces route metadataRequireTag("internal")- Enforces route tag presence
Rate Limiting
RateLimit("api", checker)- Simple key-based rate limitingRateLimitByIP(checker)- IP-based rate limiting
Custom Interceptors
Creating reusable custom interceptors is easy. We recommend using a factory pattern.
// 1. Using NewInterceptor (Named, skippable)
func FeatureEnabled(featureName string) forge.Interceptor {
return forge.NewInterceptor("feature:"+featureName, func(ctx forge.Context, route forge.RouteInfo) forge.InterceptorResult {
enabled := checkFeature(featureName)
if !enabled {
return forge.Block(forge.NotFound("Feature not available"))
}
return forge.Allow()
})
}
// 2. Using FromFunc (Anonymous, not skippable by name)
var SimpleCheck = forge.FromFunc(func(ctx forge.Context, route forge.RouteInfo) forge.InterceptorResult {
return forge.Allow()
})
// 3. Using Named (Wrap anonymous function with name)
var NamedCheck = forge.Named("my-check", SimpleCheck)
// Usage
app.Router().GET("/beta", betaHandler,
forge.WithInterceptor(FeatureEnabled("new-ui")),
)How is this guide?
Last updated on