Hooks
Configure hooks to customise the HTTP request/response lifecycle in Authula.
Overview
The hooks system in Authula provides a flexible and extensible mechanism for plugins to intercept and modify the request lifecycle. Hooks allow you to execute custom logic at specific points during request processing without tightly coupling your code to the router.
The system enables powerful capabilities such as authentication, authorization, rate limiting, logging, and custom business logic integration. Each hook operates within the request context and can modify request state, share data with other hooks, or short-circuit the request flow.
Hook Stages
Hooks execute at five distinct stages in the request lifecycle:
1. HookOnRequest
- Executes for every incoming request at the very start
- Ideal for global logging, metrics collection, or request preprocessing
- Runs before route matching and handling
2. HookBefore
- Executes before route matching and handling
- Perfect for authentication, authorization, and request validation
- Common use: checking session tokens, validating permissions
3. HookAfter
- Executes after route handling but before the response is sent
- Useful for response modification, post-processing, or cleanup
- Good for adding response headers or transforming output
4. HookOnResponse
- Executes after route handling and
HookAfter, once the response has been produced - Only runs on the normal request path; it is skipped if an earlier hook short-circuits
- Suitable for analytics, audit logging, or fire-and-forget operations
- Should not be relied on for cleanup that must run on every request
5. HookOnComplete
- Executes exactly once for every request, after all other hook stages
- Also runs after an early short-circuit, immediately before the response is flushed
- Route handlers never observe this stage
- Cannot short-circuit the request or change routing
- Always runs synchronously;
Asyncis ignored for this stage - Runs on bypass paths such as
OPTIONSpreflight and disabled-path404responses - Use it for finalization: ending telemetry spans, recording metrics, flushing per-request buffers, or releasing resources
HookOnResponse only runs on the normal path. HookOnComplete is the guaranteed finalization stage: it runs even when an earlier hook sets Handled, so it is the right place for cleanup that must happen on every exit path.
Execution order
Normal request:
HookOnRequest → HookBefore → route handler → HookAfter → HookOnResponse → HookOnComplete → flush
Short-circuited request:
HookOnRequest → HookBefore → HookOnComplete → flushConfiguration Options
Synchronization Types
Hooks can be either synchronous or asynchronous:
// Synchronous hook (default)
Hook{
Async: false, // Blocks the request flow until completion
Handler: myHandler,
}
// Asynchronous hook
Hook{
Async: true, // Runs in a background goroutine
Handler: myAsyncHandler,
}- Synchronous: Blocks the request flow and waits for completion. Use for authentication, validation, and critical security checks.
- Asynchronous: Runs in a background goroutine without blocking the response. Ideal for logging, analytics, and side effects.
Error Handling Modes
Configure how the router handles hook errors:
opts := &RouterOptions{
HookErrorMode: HookErrorModeContinue, // Default: log errors, continue execution
}HookErrorModeContinue(default): Logs errors but continues to the next hookHookErrorModeFailFast: Logs error and stops remaining hooks in the current stageHookErrorModeSilent: Silently ignores errors without logging
Async Hook Timeout
Control how long async hooks can run before being cancelled:
opts := &RouterOptions{
AsyncHookTimeout: 30 * time.Second, // Default timeout
}Hook Structure
Each hook is defined with the following properties:
type Hook struct {
Stage HookStage // When to execute (HookOnRequest, HookBefore, etc.)
PluginID string // Optional: limits execution to specific routes
Matcher HookMatcher // Optional: conditional execution based on request
Handler HookHandler // The function that executes the hook
Order int // Execution order within the same stage
Async bool // Whether to run asynchronously (ignored for HookOnComplete)
RunOnBypass bool // Also run HookOnRequest hooks on bypass paths
}Properties Explained
- Stage: Determines when the hook executes in the request lifecycle
- PluginID: If set, hook only executes if the route's metadata includes this plugin ID
- Matcher: A function that returns true/false to conditionally execute the hook
- Handler: The actual function that performs the hook's logic
- Order: Controls execution order among hooks in the same stage (lower numbers first)
- Async: Whether the hook runs synchronously or asynchronously; ignored for
HookOnComplete, which always runs synchronously - RunOnBypass: When true, a
HookOnRequesthook also runs on requests that never enter the normal lifecycle, such asOPTIONSpreflight and disabled-path404responses. These hooks must not alter routing or short-circuit.HookOnCompleteruns on those paths automatically, regardless of this field.
Practical Examples
Basic Authentication Hook
// Simple authentication hook that checks for a valid session
authHook := models.Hook{
Stage: models.HookBefore,
Order: 0,
Handler: func(ctx *models.RequestContext) error {
// Check if user is authenticated
sessionToken := ctx.Headers.Get("Authorization")
if sessionToken == "" {
ctx.SetResponse(401, nil, []byte(`{"error": "Unauthorized"}`))
ctx.Handled = true
return nil
}
// Validate session and set user info
userID, err := validateSession(sessionToken)
if err != nil {
ctx.SetResponse(401, nil, []byte(`{"error": "Invalid session"}`))
ctx.Handled = true
return nil
}
// Store user ID in context for later use
ctx.SetUserIDInContext(userID)
return nil
},
}Conditional Hook with Matcher
// Only execute hook for admin routes
adminOnlyHook := models.Hook{
Stage: models.HookBefore,
Matcher: func(reqCtx *models.RequestContext) bool {
// Only run for paths starting with /admin
return strings.HasPrefix(reqCtx.Path, "/admin")
},
Handler: func(reqCtx *models.RequestContext) error {
if reqCtx.Actor == nil || reqCtx.Actor.Type != models.ActorUser {
reqCtx.SetJSONResponse(http.StatusUnauthorized, map[string]any{"message": "Authentication required"})
reqCtx.Handled = true
return nil
}
userID := reqCtx.Actor.ID
// Check if user has admin role
isAdmin, err := hasRole(userID, "admin")
if err != nil || !isAdmin {
reqCtx.SetJSONResponse(http.StatusForbidden, map[string]any{"message": "Admin access required"})
reqCtx.Handled = true
return nil
}
return nil
},
}Data Sharing Between Hooks
// First hook sets data in the Values map
setUserDataHook := models.Hook{
Stage: models.HookBefore,
Order: 0,
Handler: func(reqCtx *models.RequestContext) error {
if reqCtx.Actor == nil || reqCtx.Actor.Type != models.ActorUser {
return nil // No user, skip
}
userID := reqCtx.Actor.ID
if userID != "" {
// Fetch user profile and store in Values for other hooks
userProfile, err := getUserProfile(userID)
if err != nil {
return err
}
reqCtx.Values["user_profile"] = userProfile
}
return nil
},
}
// Second hook reads the shared data
auditLogHook := models.Hook{
Stage: models.HookAfter,
Order: 1, // Must run after setUserDataHook
Handler: func(reqCtx *models.RequestContext) error {
if profile, exists := reqCtx.Values["user_profile"]; exists {
// Log the action with user information
logAuditEvent(profile.(UserProfile), reqCtx.Path, reqCtx.Method)
}
return nil
},
}Asynchronous Analytics Hook
// Fire-and-forget analytics hook
analyticsHook := models.Hook{
Stage: models.HookOnResponse,
Async: true,
Handler: func(reqCtx *models.RequestContext) error {
if reqCtx.Actor == nil || reqCtx.Actor.Type != models.ActorUser {
return nil // No user, skip analytics
}
// Track page views, user actions, etc.
trackEvent("page_view", map[string]interface{}{
"path": reqCtx.Path,
"method": reqCtx.Method,
"clientIP": reqCtx.ClientIP,
"userID": reqCtx.Actor.ID,
})
return nil
},
}Finalization with HookOnComplete
// Pair a start hook with a completion hook to measure every request,
// including requests rejected by an earlier authentication hook.
func (p *MyPlugin) Hooks() []models.Hook {
return []models.Hook{
{
Stage: models.HookOnRequest,
Handler: func(reqCtx *models.RequestContext) error {
reqCtx.Values["request_start"] = time.Now()
return nil
},
},
{
Stage: models.HookOnComplete,
Handler: func(reqCtx *models.RequestContext) error {
start, ok := reqCtx.Values["request_start"].(time.Time)
if !ok {
return nil
}
status := reqCtx.ResponseStatus
if status == 0 {
status = http.StatusOK
}
recordRequestDuration(reqCtx.Method, reqCtx.Path, status, time.Since(start))
return nil
},
},
}
}PluginID, Matcher, and Order still apply to HookOnComplete. The Async field is ignored, and completion hooks run on bypass paths automatically, so they do not need RunOnBypass.
Plugin-Specific Hook
// Hook that only runs for routes that include this plugin
rateLimitHook := models.Hook{
Stage: models.HookBefore,
PluginID: "ratelimit",
Order: 0,
Handler: func(reqCtx *models.RequestContext) error {
// Apply rate limiting only to routes that have "ratelimit" in their metadata
if err := checkRateLimit(reqCtx.ClientIP); err != nil {
reqCtx.SetJSONResponse(http.StatusTooManyRequests, map[string]any{"message": "Rate limit exceeded"})
reqCtx.Handled = true
return nil
}
return nil
},
}Best Practices
1. Order Matters
- Use lower
Ordervalues for security-critical hooks (authentication, authorization) - Higher
Ordervalues for less critical operations - Remember that order is local to each plugin, so compare within the same
PluginID
2. Handle Errors Gracefully
- Always return appropriate errors from your handlers
- Use
ctx.Handled = trueto short-circuit when returning an error response - Consider the error handling mode when designing your hooks
3. Use Asynchronous Hooks for Side Effects
- Use
Async: truefor logging, analytics, notifications - Keep synchronous hooks for authentication, validation, and response modification
- Be mindful of the timeout for async hooks
- Do not use
AsyncforHookOnComplete; completion hooks always run synchronously
4. Use HookOnComplete for Guaranteed Finalization
- Use
HookOnCompletefor cleanup, telemetry, metrics, and buffer flushing that must happen on every request - Do not rely on
HookAfterorHookOnResponsefor cleanup; they are skipped when an earlier hook short-circuits - Keep completion hooks fast, since they run immediately before the response is flushed
- Completion hooks cannot change routing or short-circuit the request
5. Leverage the Values Map
- Use
reqCtx.Valuesto share data between hooks - Store computed values that other hooks might need
- Avoid storing sensitive information in Values
6. Use Matchers for Conditional Logic
- Implement
Matcherfunctions to conditionally execute hooks - This is more efficient than checking conditions inside the handler
- Keep matchers lightweight to avoid performance overhead
7. Plugin Integration
- Use
PluginIDto ensure hooks only run on relevant routes - Coordinate with route metadata to enable/disable hooks per endpoint
- Follow the plugin architecture patterns for consistency
Use Cases
Authentication & Authorization
- Session validation
- JWT verification
- Role-based access control
- Permission checking
Security
- Rate limiting
- CSRF protection
- Input validation
- IP blacklisting
Monitoring & Analytics
- Request logging
- Performance metrics
- User activity tracking
- Error monitoring
- Request finalization, including telemetry span completion and duration metrics
Business Logic
- Custom validation rules
- Data enrichment
- Audit trails
- Notification triggers
The hooks system provides a powerful and flexible foundation for extending Authula's functionality while maintaining clean separation of concerns between different aspects of your authentication and authorization logic.
NOTE: this documentation explains a general overview of the hooks system but bear in mind that Authula may already contain a lot of this functionality already as plugins so you may not need to implement the things that are mentioned under "use cases" in your own project etc. So take a look at the Plugins documentation next to see what is already available!
