ginx

package module
v0.0.0-...-e80cc8e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2025 License: MIT Imports: 27 Imported by: 0

README

Ginx - Functional Middleware for Gin

Minimal, composable, and high-performance middleware toolkit for Gin, with conditional execution and functional chaining.

Features

  • Functional composition: Chain + Condition to precisely control execution
  • Production-ready: recovery, logging, timeout, CORS, auth, RBAC, cache, rate limit
  • High performance: zero-allocation conditions, token-bucket & time-window rate limiting, sharded cache
  • Clean API: unified Option/Condition pattern, easy to extend

Installation

go get github.com/simp-lee/ginx

Quick Start

package main

import (
    "time"
    "github.com/gin-gonic/gin"
    "github.com/simp-lee/ginx"
)

func main() {
    r := gin.New()

    // Basic middleware stack (recommended order)
    r.Use(ginx.NewChain().
        Use(ginx.RequestID()).                // Correlation id first
        Use(ginx.Recovery()).                 // Panic protection with logging
        Use(ginx.Logger()).                   // Structured request logging
        Use(ginx.Timeout()).                  // 30s timeout protection
        Use(ginx.CORS(ginx.WithAllowOrigins("*"))). // CORS for development
        Use(ginx.RateLimit(100, 200)).        // 100 RPS, 200 burst per IP
        Build())

    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello World"})
    })
    
    r.GET("/slow", func(c *gin.Context) {
        // This will timeout after 30 seconds due to Timeout middleware
        time.Sleep(35 * time.Second)
        c.JSON(200, gin.H{"message": "This won't be reached"})
    })

    r.Run(":8080")
}
Conditional Middleware
// Build conditional middleware chain
chain := ginx.NewChain().
    Use(ginx.RequestID()).
    Use(ginx.Recovery()).
    Use(ginx.Logger()).
    // Apply rate limiting only to API routes
    When(ginx.PathHasPrefix("/api/"), ginx.RateLimit(100, 200)).
    // Add hourly quota for API routes
    When(ginx.PathHasPrefix("/api/"), ginx.RateLimitPerHour(10000)).
    // Apply CORS only to browser requests  
    When(ginx.HeaderExists("Origin"), ginx.CORS(ginx.WithAllowOrigins("*"))).
    // Longer timeout for heavy operations
    When(ginx.PathHasPrefix("/api/heavy/"), ginx.Timeout(ginx.WithTimeout(60*time.Second)))

r.Use(chain.Build())

Core Concepts

type Middleware func(gin.HandlerFunc) gin.HandlerFunc
type Condition  func(*gin.Context) bool
type Option[T any] func(*T)
type ErrorHandler func(*gin.Context, error)
Chain (functional composition)

Chain provides fluent API for building middleware chains with conditional execution and error handling.

Chain methods:

  • NewChain() - Create new chain builder
  • Use(m Middleware) - Add middleware unconditionally
  • When(cond Condition, m Middleware) - Add middleware if condition is true
  • Unless(cond Condition, m Middleware) - Add middleware if condition is false
  • OnError(handler ErrorHandler) - Set error handler for chain execution
  • Build() - Build final gin.HandlerFunc

Note:

  • OnError is invoked only when c.Errors is non-empty. To have errors handled by the chain-level handler, call c.Error(err) in your middleware or handlers.

Example:

chain := ginx.NewChain().
  OnError(func(c *gin.Context, err error) { c.JSON(500, gin.H{"error": err.Error()}) }).
  Use(ginx.Recovery()).
  Use(ginx.Logger()).
  When(ginx.PathHasPrefix("/api/heavy"), ginx.Timeout(ginx.WithTimeout(60*time.Second))).
  Unless(ginx.PathIs("/health"), ginx.RateLimit(100, 200))

r.Use(chain.Build())
Conditions

Conditions are lightweight functions of type func(*gin.Context) bool used to decide whether middleware should execute. Most conditions are zero-allocation; ContentTypeIs parses MIME types (slight cost), and PathMatches compiles regex once at condition creation.

Logic combinators:

  • And(conds ...Condition) - All conditions must be true
  • Or(conds ...Condition) - At least one condition is true
  • Not(cond Condition) - Condition must be false

Path conditions:

  • PathIs(paths ...string) - Exact path match
  • PathHasPrefix(prefix string) - Path starts with prefix
  • PathHasSuffix(suffix string) - Path ends with suffix
  • PathMatches(pattern string) - Path matches regex pattern

HTTP conditions:

  • MethodIs(methods ...string) - HTTP method matches
  • HeaderExists(key string) - Request header exists
  • HeaderEquals(key, value string) - Header equals exact value
  • ContentTypeIs(types ...string) - Content-Type matches (MIME parsing)

Custom conditions:

  • Custom(fn func(*gin.Context) bool) - Custom condition function
  • OnTimeout() - Request has timed out

RBAC conditions (require auth):

  • IsAuthenticated() - User is authenticated
  • HasPermission(service rbac.Service, resource, action string) - Combined role + user permissions
  • HasRolePermission(service rbac.Service, resource, action string) - Role-based permissions only
  • HasUserPermission(service rbac.Service, resource, action string) - Direct user permissions only

Middleware Overview

RequestID (correlation id)

Lightweight request correlation ID middleware. It sets/propagates a unique ID via header (default: X-Request-ID) and stores it in context for logs and error handling.

Usage:

  • RequestID(options...) - Adds/propagates request id

Options:

  • WithRequestIDHeader(name) - Change header name (default: X-Request-ID)
  • WithRequestIDGenerator(func() string) - Custom ID generator
  • Default respects incoming header if present; use WithIgnoreIncoming() to always generate a new ID

Notes:

  • Logging and Recovery middlewares automatically include request_id if present
  • Place RequestID early in the chain (before Logger/Recovery) so all logs include the id
  • The middleware also echoes the ID back in the response header
Recovery (panic protection)

Graceful panic recovery middleware with intelligent error handling and structured logging.

Usage:

  • Recovery(loggerOptions...) - Basic recovery with default handler
  • RecoveryWith(handler RecoveryHandler, loggerOptions...) - Custom recovery handler

Types:

type RecoveryHandler func(*gin.Context, any)

Features:

  • Smart error detection: Distinguishes between panics and broken pipe errors
  • Structured logging: Uses github.com/simp-lee/logger with configurable options
  • Clean stack traces: Filters out recovery middleware frames and runtime panic calls
  • Broken pipe handling: Special treatment for client disconnections (warns without stack trace)
  • Custom responses: Configurable error response format via recovery handler

Default behavior:

  • Panics: Logs error + full stack trace, returns 500 JSON response
  • Broken pipes: Logs warning without stack trace, aborts connection gracefully

Example:

// Basic recovery with default handler
ginx.Recovery()

// Custom recovery handler with structured response
ginx.RecoveryWith(func(c *gin.Context, err any) {
    c.JSON(500, gin.H{
        "error": "Internal Server Error", 
        "request_id": c.GetString("request_id"),
        "timestamp": time.Now().Unix(),
    })
}, logger.WithLevel(slog.LevelError), logger.WithConsole(true))
Logger (structured logs)

Structured HTTP request logging middleware with configurable log levels and comprehensive request metadata.

Usage:

  • Logger(loggerOptions...) - HTTP request logger with configurable options

Features:

  • Smart log levels: Automatic level based on status code (5xx=Error, 4xx=Warn, others=Info)
  • Rich metadata: Method, path, query, status, latency, IP, user agent, size, protocol, referer
  • Error tracking: Separate error logging for gin context errors (when present)
  • Structured format: Uses github.com/simp-lee/logger with key-value pairs
  • Performance optimized: Single timer measurement, minimal allocations
  • Client IP detection: Uses Gin's ClientIP() method (supports proxy headers)

Example:

// Basic logging with default configuration
ginx.Logger()

// Custom log level configuration
ginx.Logger(logger.WithLevel(slog.LevelDebug), logger.WithConsole(true))
Timeout

Context-based request timeout middleware with buffered response handling to prevent partial responses.

Usage:

  • Timeout(options...) - Request timeout middleware with configurable options

Options:

  • WithTimeout(duration) - Set timeout duration (default: 30 seconds)
  • WithTimeoutResponse(response) - Set custom timeout response (default: JSON with code 408). If the value cannot be JSON-serialized, it will automatically fall back to the default 408 response.
  • WithTimeoutMessage(message) - Set timeout message (creates JSON response with code 408)

Features:

  • Atomic response handling: Buffered writer prevents partial responses during timeout
  • Context cancellation: Proper request context timeout with cancellation
  • Timeout detection: Sets X-Timeout: true header for conditional middleware
  • Zero timeout support: Immediate timeout response for zero/negative durations

Helpers:

  • IsTimeout(c *gin.Context) bool - Check if request timed out
  • Condition OnTimeout() - For conditional middleware on timeout responses

Example:

// Different timeouts for different endpoints
chain := ginx.NewChain().
    When(ginx.PathHasPrefix("/api/heavy"), 
        ginx.Timeout(ginx.WithTimeout(60*time.Second))).
    Unless(ginx.PathIs("/health"), 
        ginx.Timeout(ginx.WithTimeout(5*time.Second)))
CORS

Cross-Origin Resource Sharing (CORS) middleware with security-first design and proper preflight handling.

Usage:

  • CORS(options...) - CORS middleware with explicit origin configuration (required)
  • CORSDefault() - Development-only helper (allows all origins)

Options:

  • WithAllowOrigins(origins...) - Set allowed origins (required, no default)
  • WithAllowMethods(methods...) - Set allowed HTTP methods (default: GET, POST, PUT, DELETE, OPTIONS)
  • WithAllowHeaders(headers...) - Set allowed request headers (default: Content-Type, Authorization, Cache-Control, X-Requested-With)
  • WithExposeHeaders(headers...) - Set headers exposed to client (default: none)
  • WithAllowCredentials(allow bool) - Allow credentials like cookies/auth headers (default: false)
  • WithMaxAge(duration) - Set preflight cache duration (default: 12 hours)

Security features:

  • Explicit origins required: No default origins for security
  • Credentials validation: Prevents wildcard origins with credentials (runtime panic)
  • Proper preflight handling: Full OPTIONS request validation
  • Vary headers: Prevents proxy cache pollution

Example:

// Development: Allow all origins (use with caution)
ginx.CORS(ginx.WithAllowOrigins("*"))

// Production: Explicit security configuration
ginx.CORS(
    ginx.WithAllowOrigins("https://example.com", "https://app.example.com"),
    ginx.WithAllowHeaders("Content-Type", "Authorization"),
    ginx.WithAllowCredentials(true),
)

Security note: WithAllowCredentials(true) cannot be used with wildcard origin "*" (enforced at runtime).

Auth (JWT)

JWT authentication middleware with flexible token extraction and comprehensive context integration.

Usage:

  • Auth(jwtService jwt.Service) - JWT authentication middleware

Features:

  • Flexible token extraction: Supports both Authorization: Bearer <token> header and ?token=<token> query parameter
  • Automatic context population: Sets user ID, roles, and token metadata in gin context
  • Type-safe context keys: Uses typed context keys to prevent conflicts
  • Validation & parsing: Uses jwtService.ValidateAndParse() for comprehensive token validation

Context helpers (getters):

  • GetUserID(c) (string, bool) - Get authenticated user ID
  • GetUserRoles(c) ([]string, bool) - Get user roles from token
  • GetTokenID(c) (string, bool) - Get JWT token ID
  • GetTokenExpiresAt(c) (time.Time, bool) - Get token expiration time
  • GetTokenIssuedAt(c) (time.Time, bool) - Get token issued time
  • GetUserIDOrAbort(c) (string, bool) - Get user ID or abort with 401 if not authenticated

Context helpers (setters):

  • SetUserID(c, userID string) - Set user ID in context
  • SetUserRoles(c, roles []string) - Set user roles in context
  • SetTokenID(c, tokenID string) - Set token ID in context
  • SetTokenExpiresAt(c, expiresAt time.Time) - Set token expiration
  • SetTokenIssuedAt(c, issuedAt time.Time) - Set token issued time

Example:

jwtService, _ := jwt.New("secret-key", jwt.WithLeeway(5*time.Minute))

// Protect API routes with JWT
r.Use(ginx.NewChain().
    When(ginx.PathHasPrefix("/api/"), ginx.Auth(jwtService)).
    Build())
RBAC (Role-Based Access Control)

Role-based access control middleware with fine-grained permission checking and condition support.

Usage:

  • Middlewares (require authentication):
    • RequirePermission(service rbac.Service, resource, action string) - Check combined role + user permissions
    • RequireRolePermission(service rbac.Service, resource, action string) - Check role-based permissions only
    • RequireUserPermission(service rbac.Service, resource, action string) - Check direct user permissions only

Features:

  • Three permission models: Combined, role-only, and user-only permission checking
  • Automatic authentication check: Uses GetUserIDOrAbort() for user validation
  • Detailed error responses: Distinguishes between permission check failures (500) and access denied (403)
  • Integration with Auth: Seamlessly works with JWT authentication middleware

Conditions (for conditional middleware):

  • IsAuthenticated() - Check if user is authenticated (no service required)
  • HasPermission(service rbac.Service, resource, action string) - Check combined permissions
  • HasRolePermission(service rbac.Service, resource, action string) - Check role permissions
  • HasUserPermission(service rbac.Service, resource, action string) - Check user permissions

Error handling:

  • 500 Internal Server Error: Permission check failed (service error)
  • 403 Forbidden: Permission denied (access not allowed)
  • 401 Unauthorized: User not authenticated (handled by GetUserIDOrAbort)

Example:

rbacService, _ := rbac.New()

// Require admin permissions for admin routes
r.Use(ginx.NewChain().
    When(ginx.PathHasPrefix("/api/admin/"), 
        ginx.RequireRolePermission(rbacService, "admin", "access")).
    Build())
Cache (response caching)

HTTP-compliant response caching middleware with intelligent cache control and group support.

Usage:

  • Cache(cache shardedcache.CacheInterface) - Cache all cacheable responses (default group)
  • CacheWithGroup(cache shardedcache.CacheInterface, groupName string) - Cache with group prefix for isolation

Features:

  • HTTP-compliant caching: Respects Cache-Control: no-store/private directives
  • Smart exclusions: Automatically excludes responses with Set-Cookie headers to prevent user data leakage
  • 2xx-only caching: Only caches successful responses (200-299 status codes)
  • Efficient cache keys: Generated from HTTP method, path, and query parameters (METHOD|PATH?QUERY)
  • Response reconstruction: Preserves status code and body, and restores primary headers (multi-value headers are stored as the first value; responses with Set-Cookie are not cached)
  • Group isolation: Optional grouping for cache namespace separation

Cache key format:

GET|/api/users                    // No query parameters
POST|/api/search?q=test&limit=10  // With query parameters

Example:

cache := shardedcache.NewCache(shardedcache.Options{
    MaxSize: 1000,
    DefaultExpiration: 5 * time.Minute,
})

// Cache GET requests with version-specific grouping
r.Use(ginx.NewChain().
    When(ginx.And(ginx.MethodIs("GET"), ginx.PathHasPrefix("/api/v1/")), 
        ginx.CacheWithGroup(cache, "api-v1")).
    When(ginx.And(ginx.MethodIs("GET"), ginx.PathHasPrefix("/api/v2/")), 
        ginx.CacheWithGroup(cache, "api-v2")).
    When(ginx.And(
        ginx.MethodIs("GET"), 
        ginx.PathHasPrefix("/api/"),
        ginx.Not(ginx.Or(
            ginx.PathHasPrefix("/api/v1/"),
            ginx.PathHasPrefix("/api/v2/"),
        )),
    ), ginx.Cache(cache)).
    Build())
Rate Limit (token bucket & time windows)

High-performance rate limiting middleware supporting both token bucket (RPS) and time-window strategies (per minute/hour/day).

Token Bucket Rate Limiting (RPS)

Smooth rate limiting using token bucket algorithm for requests per second.

Usage:

  • RateLimit(rps int, burst int, opts ...RateOption) - Token bucket rate limiting with configurable options

Key generation options:

  • WithIP() - IP-based rate limiting (default behavior)
  • WithUser() - Per-user rate limiting (requires user context)
  • WithPath() - Per-path rate limiting (different limits per endpoint)
  • WithKeyFunc(keyFunc func(*gin.Context) string) - Custom key generation function

Control options:

  • WithSkipFunc(skipFunc func(*gin.Context) bool) - Skip certain requests
  • WithWait(timeout time.Duration) - Wait for tokens instead of immediate rejection
  • WithDynamicLimits(getLimits func(key string) (rps, burst int)) - Dynamic per-key limits
  • WithStore(store RateLimitStore) - Custom storage backend (default: shared memory)

Header options:

  • WithoutRateLimitHeaders() - Disable X-RateLimit-* headers
  • WithoutRetryAfterHeader() - Disable Retry-After header (enabled by default)

Features:

  • Token bucket algorithm: Smooth rate limiting using golang.org/x/time/rate
  • Multiple key strategies: IP, user ID, path, or custom key generation
  • Dynamic limits: Per-key rate limits based on user plan, endpoint type, etc.
  • Wait middleware: Traffic smoothing by waiting for available tokens
  • HTTP compliance: Standard X-RateLimit-* and Retry-After headers
  • Thread-safe: Designed for high-concurrency environments

HTTP headers:

X-RateLimit-Limit: 100              // Requests per second
X-RateLimit-Remaining: 85            // Available tokens
X-RateLimit-Reset: 1234567890        // Token bucket full reset time (Unix timestamp)
Retry-After: 3                       // Seconds to wait (429 responses only)

Note:

  • In unlimited mode (both rps and burst are <= 0), no X-RateLimit-* headers are returned.

Example:

// Basic IP-based rate limiting: 100 rps, burst 200
r.Use(ginx.RateLimit(100, 200))

// Dynamic per-user limits with wait mode
r.Use(ginx.RateLimit(0, 0,
    ginx.WithUser(),
    ginx.WithWait(2*time.Second),
    ginx.WithDynamicLimits(func(key string) (int, int) {
        if strings.HasPrefix(key, "user:premium_") { 
            return 100, 200  // Premium users
        }
        return 10, 20        // Regular users
    }),
))
Time-Window Rate Limiting (Per Minute/Hour/Day)

Fixed window rate limiting for precise quota management.

Usage:

  • RateLimitPerMinute(limit int, opts ...RateOption) - Maximum requests per minute
  • RateLimitPerHour(limit int, opts ...RateOption) - Maximum requests per hour
  • RateLimitPerDay(limit int, opts ...RateOption) - Maximum requests per day

Supported options:

  • WithIP() - IP-based limiting (default)
  • WithUser() - Per-user limiting
  • WithPath() - Per-path limiting
  • WithKeyFunc() - Custom key function
  • WithSkipFunc() - Skip certain requests
  • WithWindowStore(store WindowCounterStore) - Custom storage backend
  • WithDynamicWindowLimits(getLimit func(key string) int) - Dynamic per-key limits
  • WithoutRateLimitHeaders() - Disable headers
  • WithoutRetryAfterHeader() - Disable Retry-After header

Note: Time-window rate limiting does not support WithWait() option.

Features:

  • Fixed window algorithm: Precise quota control within time windows
  • Window reset times:
    • Minute: At 0 seconds of each minute (e.g., 14:35:00)
    • Hour: At 0 minutes of each hour (e.g., 14:00:00)
    • Day: At midnight each day (00:00:00)
  • Independent counters: Each window maintains its own counter
  • Automatic cleanup: Expired counters are automatically removed
  • Thread-safe: Designed for high-concurrency environments

HTTP headers:

X-RateLimit-Limit-Minute: 60         // Maximum per minute
X-RateLimit-Remaining-Minute: 45     // Remaining this minute
X-RateLimit-Reset-Minute: 1234567890 // Window reset time (Unix timestamp)
Retry-After: 15                      // Seconds until window resets (429 only)

Example:

// Limit to 60 requests per minute
r.Use(ginx.RateLimitPerMinute(60))

// Limit to 1000 requests per hour per user
r.Use(ginx.RateLimitPerHour(1000, ginx.WithUser()))

// Limit to 10000 requests per day
r.Use(ginx.RateLimitPerDay(10000))

// Dynamic per-user limits based on user tier
r.Use(ginx.RateLimitPerHour(0, // Base limit ignored when using dynamic limits
    ginx.WithUser(),
    ginx.WithDynamicWindowLimits(func(key string) int {
        if strings.Contains(key, "user:premium_") {
            return 100000  // Premium: 100k per hour
        }
        if strings.Contains(key, "user:pro_") {
            return 10000   // Pro: 10k per hour
        }
        return 1000        // Free: 1k per hour
    }),
))

Combine RPS and time-window rate limiting for multi-layer protection.

Usage:

// Two-layer protection: RPS + hourly quota
r.Use(ginx.NewChain().
    Use(ginx.RateLimit(10, 20)).       // Prevent instant spikes
    Use(ginx.RateLimitPerHour(1000)).  // Hourly quota management
    Build())

// Three-layer protection: RPS + hourly + daily quota
r.Use(ginx.NewChain().
    Use(ginx.RateLimit(5, 10)).        // Instant protection
    Use(ginx.RateLimitPerHour(1000)).  // Hourly quota
    Use(ginx.RateLimitPerDay(10000)).  // Daily quota
    Build())

Use cases:

  • Public APIs: Moderate RPS + daily quota
  • Premium users: High RPS + generous hourly/daily quota
  • Sensitive operations: Strict RPS + low hourly/daily quota
  • Heavy endpoints: Low RPS + low hourly quota

Response headers when combined:

X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
X-RateLimit-Reset: 1700000001      // Unix timestamp when the bucket refills
X-RateLimit-Limit-Hour: 1000
X-RateLimit-Remaining-Hour: 850
X-RateLimit-Reset-Hour: 1700003600 // Unix timestamp when the hourly window resets
Retry-After: 30

Resource management:

  • Built-in shared memory stores with automatic cleanup
  • Call ginx.CleanupRateLimiters() on application shutdown for comprehensive cleanup

Advanced Examples

Production API Server
package main

import (
    "time"
    "github.com/gin-gonic/gin"
    "github.com/simp-lee/ginx"
    "github.com/simp-lee/jwt"
    "github.com/simp-lee/rbac"
    shardedcache "github.com/simp-lee/cache"
)

func main() {
    r := gin.New()
    
    // Setup services with proper configuration
    jwtService, _ := jwt.New("your-super-secret-key-here",
        jwt.WithLeeway(5*time.Minute),
        jwt.WithIssuer("ginx-app"),
        jwt.WithMaxTokenLifetime(24*time.Hour),
    )
    rbacService, _ := rbac.New() // Default memory storage
    cache := shardedcache.NewCache(shardedcache.Options{
        MaxSize:           1000,
        DefaultExpiration: 5 * time.Minute,
        ShardCount:        16,              // Concurrent access optimization
        CleanupInterval:   1 * time.Minute, // Automatic cleanup
    })
    
    // Production middleware chain with conditional logic
    isAPIPath := ginx.PathHasPrefix("/api/")
    isPublicPath := ginx.Or(ginx.PathIs("/api/login", "/api/register"))
    isHealthPath := ginx.Or(ginx.PathIs("/health", "/metrics"))
    isAdminPath := ginx.PathHasPrefix("/api/admin/")
    
    r.Use(ginx.NewChain().
        OnError(func(c *gin.Context, err error) {
            c.JSON(500, gin.H{"error": "Internal server error"})
        }).
        // Base middleware for all requests
        Use(ginx.Recovery()).
        Use(ginx.Logger()).
        // CORS for web clients (production origins)
        Use(ginx.CORS(
            ginx.WithAllowOrigins("https://yourdomain.com", "https://app.yourdomain.com"),
            ginx.WithAllowMethods("GET", "POST", "PUT", "DELETE", "OPTIONS"),
            ginx.WithAllowHeaders("Content-Type", "Authorization"),
            ginx.WithAllowCredentials(true),
        )).
        // Different timeouts for different endpoint types
        When(ginx.PathHasPrefix("/api/heavy/"), 
            ginx.Timeout(ginx.WithTimeout(60*time.Second))).
        Unless(isHealthPath, 
            ginx.Timeout(ginx.WithTimeout(30*time.Second))).
        // Multi-layer rate limiting (skip health checks)
        When(ginx.Not(isHealthPath), 
            ginx.RateLimit(100, 200)).
        When(ginx.Not(isHealthPath), 
            ginx.RateLimitPerHour(10000)).
        When(ginx.Not(isHealthPath), 
            ginx.RateLimitPerDay(100000)).
        // JWT authentication for API routes (skip public endpoints)
        When(ginx.And(isAPIPath, ginx.Not(isPublicPath)),
            ginx.Auth(jwtService)).
        // Admin area protection
        When(isAdminPath, 
            ginx.RequirePermission(rbacService, "admin", "access")).
        // Cache GET API responses (authenticated users only)
        When(ginx.And(ginx.MethodIs("GET"), isAPIPath, ginx.IsAuthenticated()),
            ginx.Cache(cache)).
        Build())
        
    // Routes
    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "ok"})
    })
    
    api := r.Group("/api")
    {
        api.POST("/login", handleLogin)
        api.GET("/users", handleGetUsers)      // Cached
        api.POST("/users", handleCreateUser)   // Not cached
        
        admin := api.Group("/admin")
        {
            admin.GET("/stats", handleAdminStats)     // Requires admin role
            admin.DELETE("/users/:id", handleDeleteUser) // Requires admin role
        }
    }
    
    r.Run(":8080")
}
Microservice with Conditional Rate Limiting
func setupMicroservice() gin.HandlerFunc {
    return ginx.NewChain().
        Use(ginx.Recovery()).
        Use(ginx.Logger()).
        Use(ginx.Timeout(ginx.WithTimeout(10*time.Second))).
        // Different rate limits for different client types
        When(ginx.PathHasPrefix("/internal/"), 
            ginx.RateLimit(1000, 2000)).  // High limits for internal services
        When(ginx.PathHasPrefix("/api/public/"), 
            ginx.RateLimit(10, 20)).      // Low RPS for public API
        When(ginx.PathHasPrefix("/api/public/"), 
            ginx.RateLimitPerHour(1000)). // Hourly quota for public API
        When(ginx.And(
            ginx.PathHasPrefix("/api/"),
            ginx.HeaderExists("X-API-Key"),
        ), ginx.RateLimit(100, 200)).     // Medium limits for API key users
        Build()
}
Multi-tenant SaaS Application
// Per-tenant RPS rate limiting with dynamic limits based on subscription plan
r.Use(ginx.RateLimit(0, 0,
    ginx.WithUser(),  // Rate limit per user
    ginx.WithDynamicLimits(func(key string) (int, int) {
        // key format: "user:<id>"
        if strings.Contains(key, "user:premium_") {
            return 1000, 2000  // Premium users: 1000 RPS, burst 2000
        }
        if strings.Contains(key, "user:pro_") {
            return 100, 200    // Pro users: 100 RPS, burst 200
        }
        return 10, 20          // Free users: 10 RPS, burst 20
    }),
))

// Per-user hourly quotas based on subscription plan
r.Use(ginx.RateLimitPerHour(0,
    ginx.WithUser(),
    ginx.WithDynamicWindowLimits(func(key string) int {
        if strings.Contains(key, "user:premium_") {
            return 100000  // Premium: 100k per hour
        }
        if strings.Contains(key, "user:pro_") {
            return 10000   // Pro: 10k per hour
        }
        return 1000        // Free: 1k per hour
    }),
))

// Feature-based conditional access control
isAnalyticsPath := ginx.PathHasPrefix("/api/analytics/")
isBillingPath := ginx.PathHasPrefix("/api/billing/")
isReportingPath := ginx.PathHasPrefix("/api/reporting/")

r.Use(ginx.NewChain().
    // Analytics requires analytics permission
    When(isAnalyticsPath, 
        ginx.RequireRolePermission(rbacService, "analytics", "read")).
    // Billing requires billing access
    When(isBillingPath, 
        ginx.RequireRolePermission(rbacService, "billing", "access")).
    // Advanced reporting for premium users only
    When(isReportingPath,
        ginx.RequireRolePermission(rbacService, "reporting", "generate")).
    // Cache expensive analytics queries
    When(ginx.And(isAnalyticsPath, ginx.MethodIs("GET")),
        ginx.CacheWithGroup(cache, "analytics")).
    Build())
Complete Cache Strategy Example
// Real-world caching strategy with multiple cache groups and conditions
func setupAdvancedCaching(r *gin.Engine, cache shardedcache.CacheInterface) {
    // Define path conditions for clarity
    isAPIPath := ginx.PathHasPrefix("/api/")
    isPublicData := ginx.PathHasPrefix("/public/")
    isUserSpecific := ginx.PathHasPrefix("/api/users/")
    isAdminData := ginx.PathHasPrefix("/admin/")
    
    // Advanced caching chain with different strategies
    r.Use(ginx.NewChain().
        // Cache public data aggressively (separate group for easy management)
        When(ginx.And(ginx.MethodIs("GET"), isPublicData),
            ginx.CacheWithGroup(cache, "public")).
        // Cache API GET requests but exclude health/status endpoints
        When(ginx.And(
            ginx.MethodIs("GET"),
            isAPIPath,
            ginx.Not(ginx.Or(ginx.PathIs("/api/health", "/api/status"))),
        ), ginx.CacheWithGroup(cache, "api")).
        // User-specific data with separate group (privacy isolation)
        When(ginx.And(ginx.MethodIs("GET"), isUserSpecific),
            ginx.CacheWithGroup(cache, "users")).
        // Never cache admin data (add no-cache headers)
        When(isAdminData, ginx.Custom(func(c *gin.Context) bool {
            c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
            return false // Skip caching middleware entirely
        })).
        Build())
}
Combined Rate Limiting Strategies
// Multi-layer rate limiting for different scenarios
func setupRateLimiting(r *gin.Engine) {
    // Example 1: Public API with burst + quota protection
    publicAPI := r.Group("/api/public")
    publicAPI.Use(ginx.NewChain().
        Use(ginx.RateLimit(10, 20)).        // Prevent instant spikes
        Use(ginx.RateLimitPerHour(1000)).   // Hourly quota
        Use(ginx.RateLimitPerDay(10000)).   // Daily quota
        Build())

    // Example 2: Authenticated API with per-user limits
    authAPI := r.Group("/api/v1")
    authAPI.Use(ginx.NewChain().
        Use(ginx.RateLimit(50, 100, ginx.WithUser())).     // Per-user RPS
        Use(ginx.RateLimitPerHour(5000, ginx.WithUser())). // Per-user hourly quota
        Build())

    // Example 3: Heavy operations with strict limits
    r.POST("/api/heavy-task", 
        ginx.NewChain().
            Use(ginx.RateLimit(1, 1)).       // Only 1 request per second
            Use(ginx.RateLimitPerHour(10)).  // Max 10 per hour
            Use(ginx.RateLimitPerDay(50)).   // Max 50 per day
            Build(),
        handleHeavyTask)

    // Example 4: Path-based rate limiting for different endpoints
    r.Use(ginx.NewChain().
        When(ginx.PathHasPrefix("/api/search/"), ginx.NewChain().
            Use(ginx.RateLimit(5, 10, ginx.WithPath())).
            Use(ginx.RateLimitPerMinute(50, ginx.WithPath())).
            Build()).
        When(ginx.PathHasPrefix("/api/login"), ginx.NewChain().
            Use(ginx.RateLimit(1, 2, ginx.WithIP())).
            Use(ginx.RateLimitPerHour(5, ginx.WithIP())).
            Build()).
        Build())

    // Example 5: Dynamic per-user tier-based rate limiting
    r.Use(ginx.NewChain().
        // RPS based on user tier (supports dynamic limits)
        Use(ginx.RateLimit(0, 0,
            ginx.WithUser(),
            ginx.WithDynamicLimits(getUserRPSLimits))).
        // Hourly quota based on user tier
        Use(ginx.RateLimitPerHour(0,
            ginx.WithUser(),
            ginx.WithDynamicWindowLimits(getUserHourlyLimits))).
        // Daily quota based on user tier
        Use(ginx.RateLimitPerDay(0,
            ginx.WithUser(),
            ginx.WithDynamicWindowLimits(getUserDailyLimits))).
        Build())
}

func getUserRPSLimits(key string) (int, int) {
    // Extract user tier from key
    if strings.Contains(key, "user:premium_") {
        return 100, 200  // Premium: 100 RPS, burst 200
    }
    if strings.Contains(key, "user:pro_") {
        return 50, 100   // Pro: 50 RPS, burst 100
    }
    return 10, 20        // Free: 10 RPS, burst 20
}

func getUserHourlyLimits(key string) int {
    if strings.Contains(key, "user:premium_") {
        return 50000  // Premium: 50k per hour
    }
    if strings.Contains(key, "user:pro_") {
        return 5000   // Pro: 5k per hour
    }
    return 500        // Free: 500 per hour
}

func getUserDailyLimits(key string) int {
    if strings.Contains(key, "user:premium_") {
        return 1000000  // Premium: 1M per day
    }
    if strings.Contains(key, "user:pro_") {
        return 100000   // Pro: 100k per day
    }
    return 10000        // Free: 10k per day
}

Performance Notes

  • Conditions efficiency: Most conditions are zero-allocation; ContentTypeIs parses MIME (small overhead), and PathMatches compiles the regex at condition creation time (not per request).
  • Functional composition: Minimal middleware chain overhead with conditional execution
  • Sharded caching: Reduced lock contention for high-concurrency scenarios
  • Rate limiting: Token bucket for smooth RPS control, fixed window counters for quota management, both with automatic memory cleanup
  • Compiled patterns: Cached regex for PathMatches() condition

Dependencies

Core dependencies:

  • github.com/gin-gonic/gin v1.10.1 - Web framework
  • golang.org/x/time v0.13.0 - Rate limiting implementation

Optional feature dependencies:

  • github.com/simp-lee/jwt - JWT authentication (for Auth middleware)
  • github.com/simp-lee/rbac - Role-based access control (for RBAC middleware)
  • github.com/simp-lee/logger - Structured logging (for Logger/Recovery middleware)
  • github.com/simp-lee/cache - Response caching (for Cache middleware)

Testing:

  • github.com/stretchr/testify v1.11.1 - Test assertions

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertContains

func AssertContains(slice []string, item string) bool

AssertContains checks if slice contains the specified element

func AssertEqual

func AssertEqual(expected, actual interface{}) bool

AssertEqual checks if two values are equal

func AssertSliceEqual

func AssertSliceEqual(expected, actual []string) bool

AssertSliceEqual checks if two string slices are equal

func CleanupRateLimiters

func CleanupRateLimiters()

CleanupRateLimiters provides comprehensive cleanup of all rate limiter stores. It cleans up both token bucket stores and window counter stores, including default shared stores and all custom stores created with WithStore() or WithWindowStore().

Usage:

// At application shutdown
defer func() {
	ginx.CleanupRateLimiters()
}()

This function is goroutine-safe and can be called multiple times safely.

func GetRequestID

func GetRequestID(c *gin.Context) (string, bool)

GetRequestID gets the request ID from the context

func GetRequestIDFromHeader

func GetRequestIDFromHeader(r *http.Request, header string) string

Expose helper to fetch from standard header if needed (not used by middleware chain directly)

func GetTokenExpiresAt

func GetTokenExpiresAt(c *gin.Context) (time.Time, bool)

GetTokenExpiresAt gets the token expiration time from the context

func GetTokenID

func GetTokenID(c *gin.Context) (string, bool)

GetTokenID gets the token ID from the context

func GetTokenIssuedAt

func GetTokenIssuedAt(c *gin.Context) (time.Time, bool)

GetTokenIssuedAt gets the token issued time from the context

func GetUserID

func GetUserID(c *gin.Context) (string, bool)

GetUserID gets the user ID from the context

func GetUserIDOrAbort

func GetUserIDOrAbort(c *gin.Context) (string, bool)

GetUserIDOrAbort gets user ID from context or aborts with 401

func GetUserRoles

func GetUserRoles(c *gin.Context) ([]string, bool)

GetUserRoles gets the user roles from the context

func IsTimeout

func IsTimeout(c *gin.Context) bool

IsTimeout checks if the current request has timed out. Returns true if the request was terminated due to timeout.

func SetRequestID

func SetRequestID(c *gin.Context, id string)

SetRequestID sets the request ID in the context

func SetTokenExpiresAt

func SetTokenExpiresAt(c *gin.Context, expiresAt time.Time)

SetTokenExpiresAt sets the token expiration time in the context

func SetTokenID

func SetTokenID(c *gin.Context, tokenID string)

SetTokenID sets the token ID in the context

func SetTokenIssuedAt

func SetTokenIssuedAt(c *gin.Context, issuedAt time.Time)

SetTokenIssuedAt sets the token issued time in the context

func SetUserID

func SetUserID(c *gin.Context, userID string)

SetUserID sets the user ID in the context

func SetUserRoles

func SetUserRoles(c *gin.Context, roles []string)

SetUserRoles sets the user roles in the context

func TestContext

func TestContext(method, path string, headers map[string]string) (*gin.Context, *httptest.ResponseRecorder)

TestContext creates a gin.Context for testing

func TestHandler

func TestHandler(executed *[]string) gin.HandlerFunc

TestHandler creates a handler for testing

Types

type CORSConfig

type CORSConfig struct {
	AllowOrigins     []string      // Allowed origins, defaults to same-origin
	AllowMethods     []string      // Allowed methods, defaults to GET, POST, PUT, DELETE, OPTIONS
	AllowHeaders     []string      // Allowed request headers, defaults to common headers
	ExposeHeaders    []string      // Headers exposed to the client
	AllowCredentials bool          // Whether to allow credentials, defaults to false
	MaxAge           time.Duration // Preflight request cache duration, defaults to 12 hours
}

CORSConfig CORS configuration structure

type Chain

type Chain struct {
	// contains filtered or unexported fields
}

Chain is a middleware chain builder for Gin

func NewChain

func NewChain() *Chain

NewChain creates a new Chain instance

func (*Chain) Build

func (c *Chain) Build() gin.HandlerFunc

Build builds the final gin.HandlerFunc

func (*Chain) OnError

func (c *Chain) OnError(handler ErrorHandler) *Chain

OnError sets the error handler for the chain

func (*Chain) Unless

func (c *Chain) Unless(cond Condition, m Middleware) *Chain

Unless adds middleware to the chain if the condition is false

func (*Chain) Use

func (c *Chain) Use(m Middleware) *Chain

Use adds a middleware to the chain

func (*Chain) When

func (c *Chain) When(cond Condition, m Middleware) *Chain

When adds middleware to the chain if the condition is true

type Condition

type Condition func(*gin.Context) bool

Condition represents a condition function that determines whether a middleware should be executed.

func And

func And(conds ...Condition) Condition

And all conditions must be true

func ContentTypeIs

func ContentTypeIs(contentTypes ...string) Condition

ContentTypeIs checks if the Content-Type matches any of the specified types Uses precise MIME type parsing to avoid false positives

func Custom

func Custom(fn func(*gin.Context) bool) Condition

Custom creates a custom condition

func HasPermission

func HasPermission(service rbac.Service, resource, action string) Condition

HasPermission checks combined role and direct user permissions

func HasRequestID

func HasRequestID() Condition

Convenience condition: HasRequestID checks presence of request id in context

func HasRolePermission

func HasRolePermission(service rbac.Service, resource, action string) Condition

HasRolePermission checks role based permissions only

func HasUserPermission

func HasUserPermission(service rbac.Service, resource, action string) Condition

HasUserPermission checks direct user permissions only

func HeaderEquals

func HeaderEquals(key, value string) Condition

HeaderEquals checks if the request header value matches

func HeaderExists

func HeaderExists(key string) Condition

HeaderExists checks if the request header exists

func IsAuthenticated

func IsAuthenticated() Condition

IsAuthenticated checks if the user is authenticated

func MethodIs

func MethodIs(methods ...string) Condition

MethodIs checks if the HTTP method matches any of the specified methods

func Not

func Not(cond Condition) Condition

Not all conditions must be false

func OnTimeout

func OnTimeout() Condition

OnTimeout checks if the request has timed out

func Or

func Or(conds ...Condition) Condition

Or at least one condition is true

func PathHasPrefix

func PathHasPrefix(prefix string) Condition

PathHasPrefix checks if the path has the specified prefix

func PathHasSuffix

func PathHasSuffix(suffix string) Condition

PathHasSuffix checks if the path has the specified suffix

func PathIs

func PathIs(paths ...string) Condition

PathIs exact path match

func PathMatches

func PathMatches(pattern string) Condition

PathMatches checks if the path matches the specified regex pattern

type ErrorHandler

type ErrorHandler func(*gin.Context, error)

ErrorHandler represents an error handler function type.

type MemoryLimiterStore

type MemoryLimiterStore struct {
	// contains filtered or unexported fields
}

MemoryLimiterStore provides a thread-safe, in-memory implementation of RateLimitStore. It automatically cleans up expired limiters to prevent memory leaks and is registered globally for automatic resource management.

func (*MemoryLimiterStore) Clear

func (s *MemoryLimiterStore) Clear()

Clear removes all stored rate limiters and access time records.

func (*MemoryLimiterStore) Close

func (s *MemoryLimiterStore) Close() error

Close stops the cleanup goroutine and releases resources.

func (*MemoryLimiterStore) Delete

func (s *MemoryLimiterStore) Delete(key string)

Delete removes a rate limiter and its access time record.

func (*MemoryLimiterStore) Get

func (s *MemoryLimiterStore) Get(key string) (*rate.Limiter, bool)

Get retrieves a rate limiter for the given key and updates its last access time.

func (*MemoryLimiterStore) Set

func (s *MemoryLimiterStore) Set(key string, limiter *rate.Limiter)

Set stores a rate limiter for the given key and records the access time.

type MemoryWindowCounterStore

type MemoryWindowCounterStore struct {
	// contains filtered or unexported fields
}

MemoryWindowCounterStore provides a thread-safe, in-memory implementation of WindowCounterStore. It uses a fixed window algorithm for time-based rate limiting (minute/hour/day).

func (*MemoryWindowCounterStore) Clear

func (s *MemoryWindowCounterStore) Clear()

Clear removes all stored counters and access time records.

func (*MemoryWindowCounterStore) Close

func (s *MemoryWindowCounterStore) Close() error

Close stops the cleanup goroutine and releases resources.

func (*MemoryWindowCounterStore) Get

func (s *MemoryWindowCounterStore) Get(key string, window time.Time) (int64, error)

Get returns the current count for the given key and window

func (*MemoryWindowCounterStore) Increment

func (s *MemoryWindowCounterStore) Increment(key string, window time.Time) (int64, error)

Increment increments the counter for the given key and window, returns new count

func (*MemoryWindowCounterStore) IncrementWithinLimit

func (s *MemoryWindowCounterStore) IncrementWithinLimit(key string, window time.Time, limit int64) (int64, bool, error)

IncrementWithinLimit atomically increments the current window counter when under limit.

type Middleware

type Middleware func(gin.HandlerFunc) gin.HandlerFunc

Middleware represents a middleware function that takes a HandlerFunc and returns a new HandlerFunc.

func Auth

func Auth(jwtService jwt.Service) Middleware

Auth is JWT authentication middleware.

func CORS

func CORS(options ...Option[CORSConfig]) Middleware

CORS creates a CORS middleware (requires explicit origin configuration)

func CORSDefault

func CORSDefault() Middleware

CORSDefault creates a default CORS middleware (for development only)

func Cache

Cache creates a cache middleware using the default cache group

func CacheWithGroup

func CacheWithGroup(cache shardedcache.CacheInterface, groupName string) Middleware

CacheWithGroup creates a cache middleware using the specified cache group

func Logger

func Logger(options ...logger.Option) Middleware

Logger create a logging middleware with the given options.

func RateLimit

func RateLimit(rps, burst int, opts ...RateOption) Middleware

RateLimit creates a rate limiting middleware with the specified limits and options. This is the recommended way to configure rate limiting with maximum flexibility.

Resource Management: All stores (both default shared and custom stores) are automatically managed. Use CleanupRateLimiters() at application shutdown for comprehensive cleanup.

Parameters:

  • rps: Maximum requests per second allowed
  • burst: Maximum burst size (tokens that can be consumed immediately)
  • opts: Optional configuration functions

Examples:

// Basic rate limiting (uses shared global store)
r.Use(ginx.RateLimit(100, 200))

// Rate limiting by authenticated user (uses shared store)
r.Use(ginx.RateLimit(50, 100, ginx.WithUser()))

// Custom store (automatically managed)
store := ginx.NewMemoryLimiterStore(10 * time.Minute)
r.Use(ginx.RateLimit(10, 20, ginx.WithStore(store)))
// Cleanup at shutdown: ginx.CleanupRateLimiters()

// Skip rate limiting for admin users
r.Use(ginx.RateLimit(100, 200, ginx.WithSkipFunc(isAdminUser)))

func RateLimitPerDay

func RateLimitPerDay(limit int, opts ...RateOption) Middleware

RateLimitPerDay creates a rate limiting middleware that limits requests per day. Uses a fixed window algorithm (window resets at midnight).

Parameters:

  • limit: Maximum requests allowed per day (resets at midnight)
  • opts: Optional configuration functions (WithUser, WithPath, etc.)

Examples:

// Limit to 10000 requests per day
r.Use(ginx.RateLimitPerDay(10000))

// Per-user limit of 5000 requests per day
r.Use(ginx.RateLimitPerDay(5000, ginx.WithUser()))

func RateLimitPerHour

func RateLimitPerHour(limit int, opts ...RateOption) Middleware

RateLimitPerHour creates a rate limiting middleware that limits requests per hour. Uses a fixed window algorithm (window resets at 0 minutes of each hour).

Parameters:

  • limit: Maximum requests allowed per hour
  • opts: Optional configuration functions (WithUser, WithPath, etc.)

Examples:

// Limit to 1000 requests per hour
r.Use(ginx.RateLimitPerHour(1000))

// Per-user limit of 500 requests per hour
r.Use(ginx.RateLimitPerHour(500, ginx.WithUser()))

func RateLimitPerMinute

func RateLimitPerMinute(limit int, opts ...RateOption) Middleware

RateLimitPerMinute creates a rate limiting middleware that limits requests per minute. Uses a fixed window algorithm (window resets at 0 seconds of each minute).

Parameters:

  • limit: Maximum requests allowed per minute
  • opts: Optional configuration functions (WithUser, WithPath, etc.)

Examples:

// Limit to 60 requests per minute
r.Use(ginx.RateLimitPerMinute(60))

// Per-user limit of 100 requests per minute
r.Use(ginx.RateLimitPerMinute(100, ginx.WithUser()))

func Recovery

func Recovery(options ...logger.Option) Middleware

Recovery creates a panic recovery middleware.

func RecoveryWith

func RecoveryWith(handler RecoveryHandler, loggerOptions ...logger.Option) Middleware

RecoveryWith creates a panic recovery middleware with a custom handler.

func RequestID

func RequestID(opts ...RequestIDOption) Middleware

RequestID provides a simple request ID middleware. Behavior: - Read ID from Header (default: X-Request-ID) if present and RespectIncoming=true - Otherwise generate a new ID using crypto/rand (16 bytes -> 32 hex chars) - Store into gin context via SetRequestID and echo back in response header

func RequirePermission

func RequirePermission(service rbac.Service, resource, action string) Middleware

RequirePermission based on roles and direct user permission checking middleware

func RequireRolePermission

func RequireRolePermission(service rbac.Service, resource, action string) Middleware

RequireRolePermission based on role based permission only checking middleware

func RequireUserPermission

func RequireUserPermission(service rbac.Service, resource, action string) Middleware

RequireUserPermission based on direct user permission only checking middleware

func TestMiddleware

func TestMiddleware(name string, executed *[]string) Middleware

TestMiddleware creates middleware for testing that records execution state

func Timeout

func Timeout(options ...Option[TimeoutConfig]) Middleware

Timeout middleware to set a timeout for requests. This version uses a serial approach to avoid race conditions when accessing headers.

type Option

type Option[T any] func(*T)

Option represents a generic option function for configuring various structures.

func WithAllowCredentials

func WithAllowCredentials(allow bool) Option[CORSConfig]

WithAllowCredentials sets whether to allow credentials

func WithAllowHeaders

func WithAllowHeaders(headers ...string) Option[CORSConfig]

WithAllowHeaders sets the allowed request headers

func WithAllowMethods

func WithAllowMethods(methods ...string) Option[CORSConfig]

WithAllowMethods sets the allowed methods

func WithAllowOrigins

func WithAllowOrigins(origins ...string) Option[CORSConfig]

WithAllowOrigins sets the allowed origins

func WithExposeHeaders

func WithExposeHeaders(headers ...string) Option[CORSConfig]

WithExposeHeaders sets the exposed response headers

func WithMaxAge

func WithMaxAge(maxAge time.Duration) Option[CORSConfig]

WithMaxAge sets the preflight request cache duration

func WithTimeout

func WithTimeout(timeout time.Duration) Option[TimeoutConfig]

WithTimeout sets timeout duration

func WithTimeoutMessage

func WithTimeoutMessage(message string) Option[TimeoutConfig]

WithTimeoutMessage sets timeout message

func WithTimeoutResponse

func WithTimeoutResponse(response any) Option[TimeoutConfig]

WithTimeoutResponse sets timeout response content

type RateLimitStore

type RateLimitStore interface {
	// Get returns the limiter for the given key
	Get(key string) (*rate.Limiter, bool)
	// Set stores the limiter for the given key
	Set(key string, limiter *rate.Limiter)
	// Delete removes the limiter for the given key
	Delete(key string)
	// Clear removes all expired limiters
	Clear()
	// Close cleans up resources
	Close() error
}

RateLimitStore defines the interface for storing and managing rate limiters. It provides methods to store, retrieve, and manage rate.Limiter instances by key.

func NewMemoryLimiterStore

func NewMemoryLimiterStore(maxIdle time.Duration) RateLimitStore

NewMemoryLimiterStore creates a thread-safe in-memory store with automatic cleanup.

Parameters:

  • maxIdle: Duration to keep unused limiters (defaults to 5 minutes if <= 0)

Resource Management: The store is automatically registered globally and cleaned up by CleanupRateLimiters(). Manual Close() is optional unless immediate cleanup is needed.

type RateOption

type RateOption func(*rateLimiter)

RateOption configures the rate limiter behavior. Options provide a flexible way to customize rate limiting without exposing complex configuration methods.

func WithDynamicLimits

func WithDynamicLimits(getLimits func(key string) (rps int, burst int)) RateOption

WithDynamicLimits configures dynamic rate limiting where different keys can have different limits determined at runtime by the provided function. The function receives a key and should return (rps, burst) for that key. Note: When using this option, the rps and burst parameters to RateLimit are ignored as they will be determined dynamically. This option only works with RateLimit (token bucket), not with time-window rate limiting.

func WithDynamicWindowLimits

func WithDynamicWindowLimits(getLimit func(key string) int) RateOption

WithDynamicWindowLimits configures dynamic time-window rate limiting where different keys can have different limits determined at runtime by the provided function. The function receives a key and should return the limit for that key. Note: When using this option, the limit parameter to RateLimitPerMinute/Hour/Day is ignored as it will be determined dynamically. This option only works with RateLimitPerMinute/Hour/Day, not with RateLimit (token bucket).

Example:

r.Use(ginx.RateLimitPerHour(0, // Base limit ignored when using dynamic limits
    ginx.WithUser(),
    ginx.WithDynamicWindowLimits(func(key string) int {
        if strings.Contains(key, "user:premium_") {
            return 100000  // Premium: 100k per hour
        }
        if strings.Contains(key, "user:pro_") {
            return 10000   // Pro: 10k per hour
        }
        return 1000        // Free: 1k per hour
    })))

func WithIP

func WithIP() RateOption

WithIP configures rate limiting by client IP address. Each IP gets its own rate limit bucket. Note: This is the default behavior, so this option is typically redundant.

func WithKeyFunc

func WithKeyFunc(keyFunc func(*gin.Context) string) RateOption

WithKeyFunc configures a custom key generation function. The key function determines how requests are grouped for rate limiting.

func WithPath

func WithPath() RateOption

WithPath configures rate limiting by IP and path combination. This allows different rate limits for different endpoints per client.

func WithSkipFunc

func WithSkipFunc(skipFunc func(*gin.Context) bool) RateOption

WithSkipFunc configures a function to skip rate limiting for certain requests. Useful for exempting admin users, health checks, etc.

func WithStore

func WithStore(store RateLimitStore) RateOption

WithStore configures a custom storage backend for rate limiters. This allows distributed rate limiting using Redis or other systems.

Resource Management: Custom stores are automatically registered and will be cleaned up when CleanupRateLimiters() is called. Manual cleanup is optional but can be done by calling store.Close() directly if needed.

Example:

store := NewMemoryLimiterStore(10 * time.Minute)
r.Use(ginx.RateLimit(100, 200, ginx.WithStore(store)))
// Automatic cleanup at shutdown: ginx.CleanupRateLimiters()

func WithUser

func WithUser() RateOption

WithUser configures rate limiting by authenticated user ID. Falls back to IP-based limiting if no user ID is found. Users are identified by 'user_id' in the Gin context (set by auth middleware) or X-User-ID header.

func WithWait

func WithWait(timeout time.Duration) RateOption

WithWait configures the rate limiter to wait for available tokens instead of immediately rejecting requests. If the wait time exceeds the timeout, the request is rejected with a 429 status.

func WithWindowStore

func WithWindowStore(store WindowCounterStore) RateOption

WithWindowStore configures a custom storage backend for window-based rate limiters. This is used for per-minute, per-hour, and per-day rate limiting.

Resource Management: Custom window stores are automatically registered and will be cleaned up when CleanupRateLimiters() is called.

Example:

store := NewMemoryWindowCounterStore(25 * time.Hour)
r.Use(ginx.RateLimitPerHour(1000, ginx.WithWindowStore(store)))
// Automatic cleanup at shutdown: ginx.CleanupRateLimiters()

func WithoutRateLimitHeaders

func WithoutRateLimitHeaders() RateOption

WithoutRateLimitHeaders disables X-RateLimit-* headers in responses. By default, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers are included. This does NOT affect Retry-After headers.

func WithoutRetryAfterHeader

func WithoutRetryAfterHeader() RateOption

WithoutRetryAfterHeader disables Retry-After header in 429 responses. By default, Retry-After header is included in rate-limited responses as recommended by RFC 7231. Use this option only if you need to completely disable retry guidance for clients.

type RecoveryHandler

type RecoveryHandler func(*gin.Context, any)

type RequestIDConfig

type RequestIDConfig struct {
	// Header is the request/response header name to carry the ID
	// Common choices: "X-Request-ID" (default) or "Traceparent" in W3C Trace Context
	Header string

	// Generator generates a new ID when the incoming request doesn't have one
	Generator func() string

	// RespectIncoming controls whether to trust and reuse the incoming header value
	// If false, always override with a new ID
	RespectIncoming bool
}

RequestIDConfig holds configuration for the RequestID middleware

type RequestIDOption

type RequestIDOption = Option[RequestIDConfig]

RequestID options

func WithIgnoreIncoming

func WithIgnoreIncoming() RequestIDOption

WithIgnoreIncoming disables using incoming header value; always generate a new ID

func WithRequestIDGenerator

func WithRequestIDGenerator(gen func() string) RequestIDOption

WithRequestIDGenerator sets a custom ID generator

func WithRequestIDHeader

func WithRequestIDHeader(header string) RequestIDOption

WithRequestIDHeader sets the header name (default: X-Request-ID)

type TimeWindow

type TimeWindow int

TimeWindow represents different time window types for rate limiting

const (
	TimeWindowSecond TimeWindow = iota // Per second (default token bucket)
	TimeWindowMinute                   // Per minute (sliding window)
	TimeWindowHour                     // Per hour (sliding window)
	TimeWindowDay                      // Per day (sliding window)
)

type TimeoutConfig

type TimeoutConfig struct {
	Timeout  time.Duration `json:"timeout"`  // Timeout duration
	Response any           `json:"response"` // Timeout response content
}

TimeoutConfig timeout middleware configuration

type WindowCounterStore

type WindowCounterStore interface {
	// Increment increments the counter for the given key and window, returns new count
	Increment(key string, window time.Time) (int64, error)
	// IncrementWithinLimit atomically increments the counter when under limit.
	// It returns the resulting count, whether the increment happened, and any errors.
	IncrementWithinLimit(key string, window time.Time, limit int64) (count int64, allowed bool, err error)
	// Get returns the current count for the given key and window
	Get(key string, window time.Time) (int64, error)
	// Clear removes expired counters
	Clear()
	// Close cleans up resources
	Close() error
}

WindowCounterStore defines the interface for storing time-window based counters. Used for minute/hour/day rate limiting with fixed window algorithm.

func NewMemoryWindowCounterStore

func NewMemoryWindowCounterStore(maxIdle time.Duration) WindowCounterStore

NewMemoryWindowCounterStore creates a thread-safe in-memory window counter store with automatic cleanup.

Parameters:

  • maxIdle: Duration to keep unused counters (defaults to 25 hours if <= 0, sufficient for daily limits)

Resource Management: The store is automatically registered globally and cleaned up by CleanupRateLimiters().

Directories

Path Synopsis
examples
auth_rbac command
cache command
ratelimit command
requestid command
timeout command
timeout_logging command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL