Middleware provides a powerful way to inject logic into the request/response cycle, handling concerns like logging, authentication, rate limiting, compression, CORS, and more, separate from your core request handlers.
SRouter uses the standard Go http.Handler interface for middleware, often defined using the common.Middleware type alias for clarity.
// Defined in pkg/common/types.go
package common
import "net/http"
// Middleware is a function that takes an http.Handler and returns an http.Handler.
type Middleware func(http.Handler) http.HandlerA middleware function wraps an existing http.Handler (the next handler in the chain) and returns a new http.Handler that performs some action before or after calling the next handler.
Here's an example of a simple custom middleware that adds a custom header to the response:
package mymiddleware
import (
"net/http"
"github.com/Suhaibinator/SRouter/pkg/common" // Assuming common types are here
)
// AddHeaderMiddleware adds a static header to every response.
func AddHeaderMiddleware(key, value string) common.Middleware {
// Return the actual middleware function
return func(next http.Handler) http.Handler {
// Return the handler that performs the action
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Action before calling the next handler (optional)
// fmt.Println("Adding header...")
// Add the header to the response writer
w.Header().Set(key, value)
// Call the next handler in the chain
next.ServeHTTP(w, r)
// Action after calling the next handler (optional)
// fmt.Println("Header added.")
})
}
}Another example: A middleware that logs the User ID if present in the context.
package mymiddleware
import (
"fmt"
"net/http"
"github.com/Suhaibinator/SRouter/pkg/common"
"github.com/Suhaibinator/SRouter/pkg/scontext" // Use scontext for context helpers
"go.uber.org/zap" // Example logger
)
// LogUserIDMiddleware logs the user ID if authentication was successful.
// Requires an authentication middleware to run first.
// This example shows accessing UserID, but other context values (TraceID, ClientIP, Transaction, Flags)
// can be accessed similarly using their respective GetXFromRequest functions.
func LogUserIDMiddleware(logger *zap.Logger) common.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Attempt to get User ID from context
// Replace string, any with your router's actual UserIDType, UserObjectType
userID, ok := scontext.GetUserIDFromRequest[string, any](r)
// txInterface, txOK := scontext.GetTransactionFromRequest[string, any](r) // Example: Access transaction
if ok {
// Log if user ID was found
logger.Debug("Authenticated user ID found in context", zap.String("userID", userID))
fmt.Printf("[Debug] Authenticated User ID: %s for path %s\n", userID, r.URL.Path)
} else {
logger.Debug("No authenticated user ID found in context")
fmt.Printf("[Debug] No User ID for path %s\n", r.URL.Path)
}
// Call the next handler regardless
next.ServeHTTP(w, r)
})
}
}Here's an example of middleware that uses handler errors to make decisions (e.g., transaction rollback):
package mymiddleware
import (
"net/http"
"github.com/Suhaibinator/SRouter/pkg/common"
"github.com/Suhaibinator/SRouter/pkg/scontext"
"go.uber.org/zap"
)
// TransactionMiddleware demonstrates using handler errors for transaction management.
// This middleware starts a transaction before the handler and commits/rollbacks
// based on whether the handler returned an error.
func TransactionMiddleware(db *YourDBType, logger *zap.Logger) common.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Start transaction
tx := db.Begin()
if tx.Error != nil {
logger.Error("Failed to start transaction", zap.Error(tx.Error))
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Add transaction to context
txWrapper := NewTransactionWrapper(tx) // Your wrapper implementation
ctx := scontext.WithTransaction[string, any](r.Context(), txWrapper)
r = r.WithContext(ctx)
// Execute the handler
next.ServeHTTP(w, r)
// Check if handler returned an error
// Note: This only works with generic routes that store errors in context
if handlerErr, ok := scontext.GetHandlerErrorFromRequest[string, any](r); ok && handlerErr != nil {
logger.Warn("Handler error detected, rolling back transaction",
zap.Error(handlerErr))
tx.Rollback()
} else {
// No error or non-generic route, commit transaction
if err := tx.Commit().Error; err != nil {
logger.Error("Failed to commit transaction", zap.Error(err))
// Note: Response has already been written by handler
}
}
})
}
}Middleware can be applied at three levels:
- Global: Added to
RouterConfig.Middlewares. Applied to all routes handled by the router. - Sub-Router: Added to
SubRouterConfig.Middlewares. Applied to all routes within that sub-router (and its nested sub-routers), after any global middleware. - Route-Specific: Added to
RouteConfigBase.MiddlewaresorRouteConfig.Middlewares. Applied only to that specific route, after any global and sub-router middleware.
// Example applying middleware at different levels
routerConfig := router.RouterConfig{
// ... logger, etc.
Middlewares: []common.Middleware{
// middleware.CreateTraceMiddleware[T, U](idGen), // Global: Added automatically if TraceIDBufferSize > 0
// middleware.Recovery, // Global: Applied internally by SRouter
mymiddleware.AddHeaderMiddleware("X-Global", "true"), // Global: Custom middleware
// Note: Request logging is handled internally if EnableTraceLogging is true
},
SubRouters: []router.SubRouterConfig{
{
PathPrefix: "/api/v1",
Middlewares: []common.Middleware{
MyCustomAuthMiddleware(), // Sub-Router: Runs before global middleware
mymiddleware.AddHeaderMiddleware("X-API-Version", "v1"), // Sub-Router: Custom middleware
},
Routes: []router.RouteDefinition{
router.RouteConfigBase{
Path: "/users",
Methods: []router.HttpMethod{router.MethodGet},
Middlewares: []common.Middleware{
mymiddleware.LogUserIDMiddleware(logger), // Route: Runs last before handler
},
Handler: GetUsersHandler,
AuthLevel: router.Ptr(router.AuthRequired), // Example: Requires authentication
},
// ... other v1 routes
},
},
},
// ...
}
// Note: CORS preflight requests (OPTIONS with Origin header and CORS-specific headers)
// are handled at the CORS layer before reaching authentication middleware.
// Other OPTIONS requests are subject to normal authentication requirements.SRouter applies middleware in a specific order by wrapping the final handler. The effective order, from outermost (runs first) to innermost (runs last before handler), based on the internal wrapHandler and registerSubRouter logic, is:
- Recovery Middleware (Applied internally)
- Authentication Middleware (Applied internally if
AuthLevelisAuthRequiredorAuthOptional) - Rate Limiting Middleware (Applied internally if
rateLimitconfig exists) - Route-Specific and Sub-Router Middlewares (
SubRouterConfig.MiddlewaresandRouteConfig.Middlewares) - Global Middlewares (
RouterConfig.Middlewares, including Trace if enabled) - Timeout Middleware (Applied if
timeout > 0) - Your Actual Handler (
http.HandlerFuncorGenericHandler)
Note: The registerSubRouter function combines sub-router and route-specific middleware before passing the combined list to wrapHandler. wrapHandler then applies global middleware before this combined list.
Middleware within the same slice (e.g., RouterConfig.Middlewares) are applied in the order they appear in the slice; the first one in the slice becomes the outermost wrapper.
SRouter provides several built-in middleware functions and applies others internally. Refer to the source code or specific examples for exact signatures and usage.
Exported from pkg/middleware:
Recovery: Recovers from panics. Applied internally by SRouter, usually no need to add manually.MaxBodySize(limit int64): Limits request body size. Applied internally based on config, usually no need to add manually.Timeout(timeout time.Duration): Applies a request timeout using context. Applied internally based on config, usually no need to add manually.CreateTraceMiddleware[T, U](idGen *IDGenerator): Creates the trace ID middleware. Added automatically to global middleware ifRouterConfig.TraceIDBufferSize > 0. See Trace ID Integration.
RateLimit(config *common.RateLimitConfig[T, U], limiter common.RateLimiter, logger *zap.Logger): Applies rate limiting. Applied internally based on config, usually no need to add manually. See Rate Limiting.NewGormTransactionWrapper: Wrapper for GORM transactions (used withscontext). See Context Management.
Internal / Not Exported for Direct Use:
- Request Logging: Handled internally by
Router.ServeHTTPifRouterConfig.EnableTraceLoggingis true. There is no separateLoggingmiddleware to add. See Logging. - Authentication: Applied internally based on
AuthLevelconfig. See Authentication for details on authentication providers and configuration. - IP Extraction: Handled internally by
Router.ServeHTTPbased onRouterConfig.IPConfig. See IP Configuration. - CORS: Handled internally by
Router.ServeHTTPbased onRouterConfig.CORSConfig. See CORS Configuration.
Always check the specific package documentation or source code for the most up-to-date list and usage details of built-in middleware.
See the examples/middleware directory for runnable examples.