Router

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

FeatureMiddlewareInterceptors
ExecutionWraps handler (before/after)Runs before handler only
Control FlowCalls next(ctx)Returns Allow() or Block()
Primary UseCross-cutting concerns (Logging, CORS, Recovery)Gatekeeping & Enrichment (Auth, RBAC, Rate Limits)
ComplexityHigher (manages chain)Lower (single decision point)
SkippingDifficult (usually by path matching)Easy (by name)
ParallelismSequential onlySupports 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:

  1. Group Interceptors (inherited)
  2. 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.

CombinatorDescription
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 exists
  • RequireAuthProvider(name) - Checks auth.provider value
  • RequireScopes("read", "write") - Checks for required scopes
  • RequireRole("admin") - Checks for user role
  • RequireAnyScope("read", "admin") - Allows if any scope matches
  • RequireAllRoles("admin", "editor") - Checks if user has ALL roles

Validation

  • RequireHeader("X-API-Key") - Ensures header presence
  • RequireContentType("application/json") - Validates content type

Access Control

  • AllowIPs("127.0.0.1") - IP Whitelisting
  • DenyIPs("1.2.3.4") - IP Blacklisting
  • TimeWindow(9, 17, location) - Restricts access to business hours
  • Maintenance(checker) - Blocks requests when checker returns true

Feature Flags & Tenants

  • FeatureFlag("beta-features", checker) - Checks feature flag using function
  • FeatureFlagFromContext("beta-features") - Checks feature flag from context value
  • TenantIsolation("tenantId") - Ensures user can only access their tenant's data

Data Enrichment

  • Enrich("key", loader) - Loads data and adds to context
  • EnrichUser(loader) - Specialized loader for user data
  • EnrichFromService[T]("svcName", loader) - Resolves service from DI container and uses it to load data

Metadata & Tags

  • RequireMetadata("key", "value") - Enforces route metadata
  • RequireTag("internal") - Enforces route tag presence

Rate Limiting

  • RateLimit("api", checker) - Simple key-based rate limiting
  • RateLimitByIP(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