docs
core
middleware
Middleware Stack
Middleware Stack
Mithril provides a comprehensive middleware stack that handles various aspects of HTTP request processing. The middleware system is built on Fiber's middleware architecture and provides Laravel/Django-inspired functionality.
Built-in Middleware#
1. CORS (Cross-Origin Resource Sharing)
// pkg/middleware/cors.go
func CORS() fiber.Handler {
return cors.New(cors.Config{
AllowOrigins: config.Get("CORS_ALLOWED_ORIGINS", "http://localhost:3000"),
AllowMethods: config.Get("CORS_ALLOWED_METHODS", "GET,POST,PUT,DELETE,OPTIONS"),
AllowHeaders: config.Get("CORS_ALLOWED_HEADERS", "Content-Type,Authorization"),
ExposeHeaders: config.Get("CORS_EXPOSED_HEADERS", ""),
MaxAge: config.GetInt("CORS_MAX_AGE", 86400),
AllowCredentials: config.GetBool("CORS_ALLOW_CREDENTIALS", true),
})
}
Configuration:
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://myapp.com
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=Content-Type,Authorization,X-Requested-With
CORS_EXPOSED_HEADERS=X-Total-Count
CORS_MAX_AGE=86400
CORS_ALLOW_CREDENTIALS=true
2. CSRF Protection
// pkg/middleware/csrf.go
func CSRF() fiber.Handler {
return csrf.New(csrf.Config{
KeyLookup: "header:X-CSRF-Token",
CookieName: "csrf_",
CookieSameSite: "Lax",
Expiration: time.Hour * 24,
KeyGenerator: utils.GenerateCSRFKey,
})
}
Usage:
// In your routes
app.Use(middleware.CSRF())
// In your frontend
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
fetch('/api/data', {
method: 'POST',
headers: {
'X-CSRF-Token': token,
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
3. Rate Limiting
// pkg/middleware/ratelimit.go
func RateLimit() fiber.Handler {
return limiter.New(limiter.Config{
Max: config.GetInt("RATE_LIMIT_MAX_REQUESTS", 100),
Expiration: time.Duration(config.GetInt("RATE_LIMIT_WINDOW_MINUTES", 15)) * time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
return c.IP()
},
LimitReached: func(c *fiber.Ctx) error {
return c.Status(429).JSON(fiber.Map{
"error": "Too Many Requests",
"retry_after": c.Get("Retry-After"),
})
},
})
}
Configuration:
RATE_LIMIT_ENABLED=true
RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_WINDOW_MINUTES=15
RATE_LIMIT_SKIP_SUCCESSFUL_REQUESTS=false
RATE_LIMIT_SKIP_FAILED_REQUESTS=false
4. Session Management
// pkg/middleware/session.go
func Session() fiber.Handler {
return session.New(session.Config{
Storage: getSessionStorage(),
KeyLookup: "cookie:session_id",
CookieName: "session",
Expiration: time.Duration(config.GetInt("SESSION_LIFETIME", 120)) * time.Minute,
Secure: config.GetBool("SESSION_SECURE", false),
HTTPOnly: config.GetBool("SESSION_HTTP_ONLY", true),
SameSite: config.Get("SESSION_SAME_SITE", "Lax"),
})
}
func getSessionStorage() fiber.Storage {
driver := config.Get("SESSION_DRIVER", "memory")
switch driver {
case "database":
return getDatabaseStorage()
case "redis":
return getRedisStorage()
default:
return memory.New()
}
}
Configuration:
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=
SESSION_SECURE=false
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=Lax
5. Authentication Middleware
// pkg/middleware/auth.go
func Auth() fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
}
// Remove "Bearer " prefix
if len(token) > 7 && token[:7] == "Bearer " {
token = token[7:]
}
// Validate JWT token
claims, err := jwt.ValidateToken(token)
if err != nil {
return c.Status(401).JSON(fiber.Map{"error": "Invalid token"})
}
// Set user in context
c.Locals("user", claims.User)
c.Locals("user_id", claims.UserID)
return c.Next()
}
}
func OptionalAuth() fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token != "" {
if len(token) > 7 && token[:7] == "Bearer " {
token = token[7:]
}
if claims, err := jwt.ValidateToken(token); err == nil {
c.Locals("user", claims.User)
c.Locals("user_id", claims.UserID)
}
}
return c.Next()
}
}
6. Role-Based Access Control (RBAC)
// pkg/middleware/rbac.go
func RequireRole(roles ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
user := c.Locals("user")
if user == nil {
return c.Status(401).JSON(fiber.Map{"error": "Authentication required"})
}
userRoles := getUserRoles(user.(*models.User))
for _, role := range roles {
if contains(userRoles, role) {
return c.Next()
}
}
return c.Status(403).JSON(fiber.Map{"error": "Insufficient permissions"})
}
}
func RequirePermission(permissions ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
user := c.Locals("user")
if user == nil {
return c.Status(401).JSON(fiber.Map{"error": "Authentication required"})
}
userPermissions := getUserPermissions(user.(*models.User))
for _, permission := range permissions {
if contains(userPermissions, permission) {
return c.Next()
}
}
return c.Status(403).JSON(fiber.Map{"error": "Insufficient permissions"})
}
}
Usage:
// Require specific role
app.Get("/admin", middleware.Auth(), middleware.RequireRole("admin"), adminHandler)
// Require specific permission
app.Post("/posts", middleware.Auth(), middleware.RequirePermission("create_posts"), createPostHandler)
// Multiple roles (OR logic)
app.Get("/moderator", middleware.Auth(), middleware.RequireRole("admin", "moderator"), moderatorHandler)
7. Security Headers
// pkg/middleware/security.go
func Security() fiber.Handler {
return func(c *fiber.Ctx) error {
// X-Content-Type-Options
c.Set("X-Content-Type-Options", "nosniff")
// X-Frame-Options
c.Set("X-Frame-Options", "DENY")
// X-XSS-Protection
c.Set("X-XSS-Protection", "1; mode=block")
// Referrer-Policy
c.Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Content-Security-Policy
csp := "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
c.Set("Content-Security-Policy", csp)
// Strict-Transport-Security (HTTPS only)
if c.Secure() {
c.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
return c.Next()
}
}
8. Request ID
// pkg/middleware/request_id.go
func RequestID() fiber.Handler {
return func(c *fiber.Ctx) error {
requestID := c.Get("X-Request-ID")
if requestID == "" {
requestID = utils.GenerateRequestID()
}
c.Set("X-Request-ID", requestID)
c.Locals("request_id", requestID)
return c.Next()
}
}
9. Logger Middleware
// pkg/middleware/logger.go
func Logger() fiber.Handler {
return logger.New(logger.Config{
Format: "${time} ${status} - ${method} ${path} (${ip}) ${latency}\n",
TimeFormat: "2006-01-02 15:04:05",
TimeZone: "UTC",
Output: os.Stdout,
})
}
func StructuredLogger() fiber.Handler {
return func(c *fiber.Ctx) error {
start := time.Now()
err := c.Next()
log := map[string]interface{}{
"timestamp": time.Now().Format(time.RFC3339),
"method": c.Method(),
"path": c.Path(),
"status": c.Response().StatusCode(),
"latency": time.Since(start).String(),
"ip": c.IP(),
"user_agent": c.Get("User-Agent"),
"request_id": c.Locals("request_id"),
}
if user := c.Locals("user"); user != nil {
log["user_id"] = user.(*models.User).ID
}
if err != nil {
log["error"] = err.Error()
}
jsonLog, _ := json.Marshal(log)
fmt.Println(string(jsonLog))
return err
}
}
10. Recovery Middleware
// pkg/middleware/recovery.go
func Recovery() fiber.Handler {
return recovery.New(recovery.Config{
EnableStackTrace: true,
StackTraceHandler: func(c *fiber.Ctx, e interface{}) {
log.Printf("Panic recovered: %v\n%s", e, debug.Stack())
},
})
}
11. Compression
// pkg/middleware/compression.go
func Compression() fiber.Handler {
return compress.New(compress.Config{
Level: compress.LevelBestSpeed,
})
}
Custom Middleware#
Creating Custom Middleware
// app/middleware/custom_middleware.go
package middleware
import (
"github.com/gofiber/fiber/v2"
"time"
)
func CustomMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
// Before request processing
start := time.Now()
// Process request
err := c.Next()
// After request processing
duration := time.Since(start)
// Log or perform other operations
if duration > time.Second {
// Log slow requests
}
return err
}
}
Middleware with Configuration
// app/middleware/api_key.go
package middleware
import (
"github.com/gofiber/fiber/v2"
"strings"
)
type APIKeyConfig struct {
HeaderName string
ValidKeys []string
}
func APIKey(config APIKeyConfig) fiber.Handler {
return func(c *fiber.Ctx) error {
apiKey := c.Get(config.HeaderName)
if apiKey == "" {
return c.Status(401).JSON(fiber.Map{"error": "API key required"})
}
for _, validKey := range config.ValidKeys {
if apiKey == validKey {
return c.Next()
}
}
return c.Status(401).JSON(fiber.Map{"error": "Invalid API key"})
}
}
Usage:
app.Use(middleware.APIKey(middleware.APIKeyConfig{
HeaderName: "X-API-Key",
ValidKeys: []string{"key1", "key2", "key3"},
}))
Middleware Order#
The order of middleware is important. Here's the recommended order:
func setupMiddleware(app *fiber.App) {
// 1. Recovery (first to catch panics)
app.Use(middleware.Recovery())
// 2. Request ID (for tracing)
app.Use(middleware.RequestID())
// 3. Logger (for request logging)
app.Use(middleware.StructuredLogger())
// 4. Security headers
app.Use(middleware.Security())
// 5. Compression
app.Use(middleware.Compression())
// 6. CORS
app.Use(middleware.CORS())
// 7. Rate limiting
app.Use(middleware.RateLimit())
// 8. Session management
app.Use(middleware.Session())
// 9. CSRF protection (after session)
app.Use(middleware.CSRF())
// 10. Custom middleware
app.Use(middleware.CustomMiddleware())
}
Route-Specific Middleware#
// Apply middleware to specific routes
app.Get("/public", publicHandler)
// Protected routes
app.Use(middleware.Auth())
app.Get("/protected", protectedHandler)
// Admin routes
app.Use(middleware.RequireRole("admin"))
app.Get("/admin", adminHandler)
// API routes with different middleware
api := app.Group("/api", middleware.APIKey(config))
api.Get("/data", dataHandler)
Middleware Testing#
// pkg/testing/middleware_test.go
func TestAuthMiddleware(t *testing.T) {
app := fiber.New()
app.Use(middleware.Auth())
app.Get("/test", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"user": c.Locals("user")})
})
// Test without token
req := httptest.NewRequest("GET", "/test", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
// Test with valid token
token := generateTestToken()
req = httptest.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err = app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
Configuration#
All middleware can be configured through environment variables:
# CORS Configuration
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://myapp.com
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=Content-Type,Authorization,X-Requested-With
CORS_EXPOSED_HEADERS=X-Total-Count
CORS_MAX_AGE=86400
CORS_ALLOW_CREDENTIALS=true
# Rate Limiting
RATE_LIMIT_ENABLED=true
RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_WINDOW_MINUTES=15
RATE_LIMIT_SKIP_SUCCESSFUL_REQUESTS=false
RATE_LIMIT_SKIP_FAILED_REQUESTS=false
# Session Configuration
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=
SESSION_SECURE=false
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=Lax
# Security Headers
SECURITY_HEADERS_ENABLED=true
CSP_ENABLED=true
HSTS_ENABLED=true
Best Practices#
1. Middleware Order
- Place recovery middleware first
- Add logging and request ID early
- Apply security headers before business logic
- Use authentication before authorization
2. Performance
- Use compression for text responses
- Implement proper caching headers
- Avoid expensive operations in middleware
- Use connection pooling for database middleware
3. Security
- Always validate input
- Use HTTPS in production
- Implement proper CORS policies
- Apply rate limiting appropriately
4. Error Handling
- Use structured logging
- Don't expose sensitive information
- Implement proper error responses
- Use request IDs for tracing