Documentation

Middleware

Overview

Middleware provides a way to filter and modify HTTP requests entering your application. The framework includes built-in middleware and supports custom middleware creation.

Built-in Middleware

Authentication Middleware

// JWT Authentication
func JWTAuthMiddleware(secretKey string) gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenString := c.GetHeader("Authorization")

        if tokenString == "" {
            c.JSON(401, gin.H{"error": "Missing authorization token"})
            c.Abort()
            return
        }

        // Remove "Bearer " prefix
        if strings.HasPrefix(tokenString, "Bearer ") {
            tokenString = tokenString[7:]
        }

        token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
            return []byte(secretKey), nil
        })

        if err != nil || !token.Valid {
            c.JSON(401, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }

        // Store user info in context
        if claims, ok := token.Claims.(jwt.MapClaims); ok {
            c.Set("user_id", claims["user_id"])
            c.Set("user_email", claims["email"])
        }

        c.Next()
    }
}

CORS Middleware

func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Credentials", "true")
        c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }

        c.Next()
    }
}

Logging Middleware

func LoggingMiddleware(logger *log.Logger) gin.HandlerFunc {
    return gin.LoggerWithConfig(gin.LoggerConfig{
        Output: logger.Writer(),
        Formatter: func(param gin.LogFormatterParams) string {
            return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
                param.ClientIP,
                param.TimeStamp.Format(time.RFC1123),
                param.Method,
                param.Path,
                param.Request.Proto,
                param.StatusCode,
                param.Latency,
                param.Request.UserAgent(),
                param.ErrorMessage,
            )
        },
    })
}

Rate Limiting Middleware

func RateLimitMiddleware(requests int, window time.Duration) gin.HandlerFunc {
    type client struct {
        requests int
        window   time.Time
    }

    clients := make(map[string]*client)
    mutex := &sync.Mutex{}

    return func(c *gin.Context) {
        ip := c.ClientIP()
        mutex.Lock()
        defer mutex.Unlock()

        now := time.Now()

        // Clean up old entries
        if client, exists := clients[ip]; exists {
            if now.Sub(client.window) > window {
                client.requests = 0
                client.window = now
            }
        } else {
            clients[ip] = &client{0, now}
        }

        client := clients[ip]
        client.requests++

        if client.requests > requests {
            c.JSON(429, gin.H{"error": "Rate limit exceeded"})
            c.Abort()
            return
        }

        c.Next()
    }
}

Custom Middleware

Creating Custom Middleware

func CustomAuthMiddleware(userFetcher *srv.UserFetcher) gin.HandlerFunc {
    return func(c *gin.Context) {
        userId := c.GetHeader("X-User-ID")
        if userId == "" {
            c.JSON(401, gin.H{"error": "User ID required"})
            c.Abort()
            return
        }

        user, exists, err := userFetcher.FindOneById(c.Request.Context(), userId, nil)
        if err != nil {
            c.JSON(500, gin.H{"error": "Database error"})
            c.Abort()
            return
        }

        if !exists {
            c.JSON(404, gin.H{"error": "User not found"})
            c.Abort()
            return
        }

        if !user.Active {
            c.JSON(403, gin.H{"error": "Account disabled"})
            c.Abort()
            return
        }

        // Store user in context
        c.Set("current_user", user)
        c.Next()
    }
}

Validation Middleware

func ValidateJSONMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.Request.Header.Get("Content-Type") == "application/json" {
            var json map[string]interface{}
            if err := c.ShouldBindJSON(&json); err != nil {
                c.JSON(400, gin.H{"error": "Invalid JSON format"})
                c.Abort()
                return
            }
            // Re-bind the JSON data
            c.Set("json_data", json)
        }
        c.Next()
    }
}

Error Handling Middleware

func ErrorHandlerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("Panic recovered: %v", err)
                c.JSON(500, gin.H{"error": "Internal server error"})
                c.Abort()
            }
        }()

        c.Next()

        // Handle any errors that were set during request processing
        if len(c.Errors) > 0 {
            err := c.Errors.Last().Err
            switch e := err.(type) {
            case *ValidationError:
                c.JSON(400, gin.H{"error": e.Message, "field": e.Field})
            case *NotFoundError:
                c.JSON(404, gin.H{"error": "Resource not found"})
            default:
                c.JSON(500, gin.H{"error": "Internal server error"})
            }
            c.Abort()
        }
    }
}

Middleware Registration

Global Middleware

func SetupRouter(container *container.Container) *gin.Engine {
    router := gin.New()

    // Global middleware
    router.Use(CORSMiddleware())
    router.Use(LoggingMiddleware(container.Logger))
    router.Use(ErrorHandlerMiddleware())
    router.Use(RateLimitMiddleware(100, time.Minute))

    // Register routes
    RegisterRoutes(router, container)

    return router
}

Group Middleware

func RegisterAPIRoutes(router *gin.Engine, container *container.Container) {
    // API routes with authentication
    api := router.Group("/api/v1")
    api.Use(JWTAuthMiddleware(container.Config.JWTSecret))
    api.Use(ValidateJSONMiddleware())
    {
        api.GET("/gardens", container.GardenController.ApiList)
        api.POST("/gardens", container.GardenController.ApiCreate)
        api.GET("/gardens/:id", container.GardenController.ApiShow)
    }

    // Public API routes
    public := router.Group("/public")
    {
        public.GET("/health", container.HealthController.Check)
        public.POST("/auth/login", container.AuthController.Login)
    }
}

Route-Specific Middleware

func RegisterAdminRoutes(router *gin.Engine, container *container.Container) {
    admin := router.Group("/admin")
    admin.Use(JWTAuthMiddleware(container.Config.JWTSecret))
    admin.Use(AdminRequiredMiddleware())
    {
        // Only admin users can access these routes
        admin.GET("/users", container.UserController.AdminList)
        admin.DELETE("/users/:id", container.UserController.AdminDelete)
    }
}

func AdminRequiredMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        user, exists := c.Get("current_user")
        if !exists {
            c.JSON(403, gin.H{"error": "Access denied"})
            c.Abort()
            return
        }

        if !user.(*mdl.User).IsAdmin {
            c.JSON(403, gin.H{"error": "Admin access required"})
            c.Abort()
            return
        }

        c.Next()
    }
}

Middleware Chain

Understanding Execution Order

router.Use(Middleware1()) // Executes first (before request)
router.Use(Middleware2()) // Executes second (before request)
router.Use(Middleware3()) // Executes third (before request)

router.GET("/endpoint", Handler) // Main handler

// After handler completes:
// Middleware3() continues (after request)
// Middleware2() continues (after request)
// Middleware1() continues (after request)

Conditional Middleware

func ConditionalMiddleware(condition func(*gin.Context) bool, middleware gin.HandlerFunc) gin.HandlerFunc {
    return func(c *gin.Context) {
        if condition(c) {
            middleware(c)
        } else {
            c.Next()
        }
    }
}

// Usage
router.Use(ConditionalMiddleware(
    func(c *gin.Context) bool {
        return strings.HasPrefix(c.Request.URL.Path, "/api/")
    },
    JWTAuthMiddleware(secretKey),
))

Testing Middleware

Unit Testing

func TestJWTAuthMiddleware(t *testing.T) {
    // Setup
    router := gin.New()
    router.Use(JWTAuthMiddleware("test-secret"))
    router.GET("/protected", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "success"})
    })

    // Test missing token
    w := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/protected", nil)
    router.ServeHTTP(w, req)

    assert.Equal(t, 401, w.Code)

    // Test valid token
    token := createTestJWT("test-secret", "user123")
    w = httptest.NewRecorder()
    req = httptest.NewRequest("GET", "/protected", nil)
    req.Header.Set("Authorization", "Bearer "+token)
    router.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
}

Integration Testing

func TestMiddlewareChain(t *testing.T) {
    container := setupTestContainer()
    router := SetupRouter(container)

    // Test that all middleware executes correctly
    w := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/api/v1/gardens", nil)
    req.Header.Set("Authorization", "Bearer "+validToken)
    req.Header.Set("Content-Type", "application/json")

    router.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.Contains(t, w.Header().Get("Access-Control-Allow-Origin"), "*")
}

Best Practices

Performance Considerations

// Cache expensive operations
func CachedAuthMiddleware(cache *sync.Map) gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")

        // Check cache first
        if userData, exists := cache.Load(token); exists {
            c.Set("user", userData)
            c.Next()
            return
        }

        // Validate and cache
        user := validateToken(token)
        cache.Store(token, user)
        c.Set("user", user)
        c.Next()
    }
}

Error Handling

func SafeMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("Middleware panic: %v", r)
                if !c.Writer.Written() {
                    c.JSON(500, gin.H{"error": "Internal server error"})
                }
            }
        }()

        c.Next()
    }
}

Configuration

type MiddlewareConfig struct {
    EnableCORS      bool
    EnableAuth      bool
    RateLimit       int
    JWTSecret       string
    AllowedOrigins  []string
}

func SetupMiddleware(router *gin.Engine, config *MiddlewareConfig) {
    if config.EnableCORS {
        router.Use(CORSMiddleware(config.AllowedOrigins))
    }

    if config.RateLimit > 0 {
        router.Use(RateLimitMiddleware(config.RateLimit, time.Minute))
    }

    if config.EnableAuth {
        router.Use(JWTAuthMiddleware(config.JWTSecret))
    }
}

Middleware provides a powerful way to add cross-cutting concerns to your application while keeping your handlers focused on their core business logic.