ginx

package module
v0.0.0-...-2c96d21 Latest Latest
Warning

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

Go to latest
Published: Feb 20, 2026 License: MIT Imports: 34 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
  • Unified error formatting: one ErrorFormatter controls all middleware error responses
  • 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)
type ErrorFormatter  func(status int, message string) any
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
  • WithErrorFormat(f ErrorFormatter) - Set unified error response format for all middleware in the chain
  • 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
  • HasRequestID() - Request has a request ID set in context

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

ErrorFormatter (unified error responses)

Unified error response formatting for all middleware. Instead of configuring error responses per-middleware, a single ErrorFormatter function controls how every middleware (auth, RBAC, timeout, rate limit, recovery) renders its error response.

Usage:

  • Chain.WithErrorFormat(f ErrorFormatter) - Set formatter for an entire chain
  • ErrorFormat(f ErrorFormatter) - Standalone middleware (for use without Chain)

Context helpers:

  • SetErrorFormatter(c *gin.Context, f ErrorFormatter) - Set formatter in context
  • GetErrorFormatter(c *gin.Context) ErrorFormatter - Get formatter from context (nil if not set)
  • AbortWithError(c *gin.Context, status int, message string) - Write error response using the formatter, or fall back to {"error": "<message>"}

Default behavior (no formatter set):

{"error": "request timeout"}

Example with Chain:

r.Use(ginx.NewChain().
    WithErrorFormat(func(status int, msg string) any {
        return gin.H{
            "code":    status,
            "message": msg,
            "success": false,
        }
    }).
    Use(ginx.Recovery()).
    Use(ginx.Logger()).
    Use(ginx.Timeout(ginx.WithTimeout(10 * time.Second))).
    Use(ginx.RateLimit(100, 200)).
    Build())

// All middleware errors now return:
// {"code": 429, "message": "rate limit exceeded", "success": false}
// {"code": 408, "message": "request timeout", "success": false}
// etc.

Example with standalone middleware (no Chain):

// Works with standard Gin middleware registration
r.Use(ginx.ErrorFormat(func(status int, msg string) any {
    return gin.H{"code": status, "message": msg}
})(func(c *gin.Context) { c.Next() }))

r.Use(ginx.Timeout(ginx.WithTimeout(10 * time.Second))(func(c *gin.Context) { c.Next() }))

Example per route group:

// Different formats for different API versions
v1 := r.Group("/api/v1")
v1.Use(ginx.NewChain().
    WithErrorFormat(func(status int, msg string) any {
        return gin.H{"error": msg, "status": status}
    }).
    Use(ginx.RateLimit(100, 200)).
    Build())

v2 := r.Group("/api/v2")
v2.Use(ginx.NewChain().
    WithErrorFormat(func(status int, msg string) any {
        return gin.H{"code": status, "message": msg, "ok": false}
    }).
    Use(ginx.RateLimit(100, 200)).
    Build())

Notes:

  • One ErrorFormatter replaces the need for per-middleware response options
  • All middleware use AbortWithError internally, so the formatter applies uniformly
  • When no formatter is set, the default response is {"error": "<message>"}
RequestID (correlation id)

Lightweight request correlation ID middleware. It sets/propagates a unique ID via header (default: X-Request-ID), stores it in Gin context, and can optionally inject the ID into Go's standard context.Context.

Usage:

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

Options:

  • WithRequestIDHeader(name) - Change header name (default: X-Request-ID)
  • WithRequestIDGenerator(func() string) - Custom ID generator
  • WithContextInjector(func(ctx context.Context, requestID string) context.Context) - Inject request metadata into Go context (for service/repository logging with slog.InfoContext etc.)
  • Default respects incoming header if present; use WithIgnoreIncoming() to always generate a new ID

Type:

type ContextInjector func(ctx context.Context, requestID string) context.Context

Context helpers:

  • SetRequestID(c, id string) - Set request ID in context
  • GetRequestID(c) (string, bool) - Get request ID from context

Utility:

  • GetRequestIDFromHeader(r *http.Request, header string) string - Extract request ID from HTTP request header

Condition:

  • HasRequestID() - Check if request has a request ID set in context

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

Example: inject into Go context (for context-aware slog):

package main

import (
    "context"
    "log/slog"

    "github.com/gin-gonic/gin"
    "github.com/simp-lee/ginx"
    "github.com/simp-lee/logger"
)

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

    r.Use(ginx.RequestID(
        ginx.WithContextInjector(func(ctx context.Context, id string) context.Context {
            return logger.WithContextAttrs(ctx, slog.String("request_id", id))
        }),
    ))

    r.GET("/users", func(c *gin.Context) {
        // Service/repository layers can now read request_id from c.Request.Context()
        // and emit it automatically with slog.InfoContext.
        slog.InfoContext(c.Request.Context(), "list users")
        c.JSON(200, gin.H{"ok": true})
    })
}
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) {
    rid, _ := ginx.GetRequestID(c)
    c.JSON(500, gin.H{
        "error": "Internal Server Error", 
        "request_id": rid,
        "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
  • Query sanitization: Automatically redacts sensitive query parameters (token, access_token, id_token, jwt, authorization, auth, password, secret)
  • 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)
  • WithMaxBufferSize(size int) - Set maximum response buffer size in bytes (default: 0 = unlimited)

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() - Check X-Timeout header (pre-execution condition; not suitable for post-result timeout detection)

Important timing note:

  • OnTimeout() is evaluated before the wrapped middleware executes, so it cannot reliably detect timeouts that are decided later in the request lifecycle.
  • To detect timeout outcomes, use IsTimeout(c) after c.Next() in outer middleware.
  • Inside a timeout-protected handler, use c.Request.Context().Done() / c.Request.Context().Err() to stop work early.

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 secure-by-default token extraction and comprehensive context integration.

Usage:

  • Auth(jwtService jwt.Service, options ...Option[AuthConfig]) - JWT authentication middleware
  • WithAuthQueryToken(true) - Explicitly enable ?token= query fallback (disabled by default)

Features:

  • Secure default token extraction: Uses Authorization: Bearer <token> by default
  • Optional query fallback: ?token=<token> is only enabled with WithAuthQueryToken(true)
  • 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
  • CacheWithOptions(cache shardedcache.CacheInterface, opts ...CacheOption) - Cache with custom options (default group)
  • CacheWithGroupOptions(cache shardedcache.CacheInterface, groupName string, opts ...CacheOption) - Grouped cache with custom options

Features:

  • HTTP-compliant caching: Respects Cache-Control: no-store/private/no-cache/must-revalidate/max-age=0 directives
  • Smart exclusions: Automatically excludes responses with Set-Cookie headers to prevent user data leakage
  • Auth/session-safe default: Skips caching when request contains Authorization or Cookie header
  • Range-safe: Bypasses cache for Range requests and Content-Range responses (partial content)
  • 2xx-only caching: Only caches successful responses (200-299, excluding 206 Partial Content)
  • GET & HEAD support: Caches both GET and HEAD responses; only body is omitted on HEAD replay
  • Safer default cache keys: Generated from HTTP method + host + path + query (METHOD|HOST|PATH?QUERY)
  • Content negotiation safety: Default keys include Accept-Encoding variant when present to avoid representation mix-ups
  • Configurable key strategy: WithCacheKeyFunc(func(*gin.Context) string) supports custom variant/context dimensions
  • Configurable vary dimensions: WithCacheVaryHeaders(headers...) extends/overrides header dimensions used by default key generation
  • Response reconstruction: Preserves status code/body and replays full response headers including multi-value headers (Link, Vary, etc.); responses with Set-Cookie are not cached
  • Group isolation: Optional grouping for cache namespace separation

Cache key format:

GET|api.example.com|/api/users                         // No query parameters
GET|api.example.com|/api/search?q=test&limit=10        // With query parameters
GET|api.example.com|/api/users|h:Accept-Encoding=gzip  // Content-encoding variant

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 using authenticated context (user_id)
  • WithTrustedUserHeader(name) - Optional trusted header fallback for gateway-injected identity
  • WithPath() - Per-path rate limiting (different limits per endpoint)
  • WithKeyFunc(keyFunc func(*gin.Context) string) - Custom key generation function

Security note:

  • WithUser() does not trust client-provided headers.
  • Use WithTrustedUserHeader(name) only when the header is set by trusted infrastructure (API gateway/auth proxy) and cannot be spoofed by clients.

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; see Custom Storage Backends)

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 (see Custom Storage Backends)
  • 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
Custom Storage Backends

For advanced scenarios (e.g. Redis-backed rate limiting), implement the exported store interfaces:

Token bucket store:

// RateLimitStore defines the interface for storing and managing rate limiters.
type RateLimitStore interface {
    Get(key string) (*rate.Limiter, bool)
    Set(key string, limiter *rate.Limiter)
    Delete(key string)
    Clear()
    Close() error
}

Time-window counter store:

// WindowCounterStore defines the interface for storing time-window based counters.
type WindowCounterStore interface {
    Increment(key string, window time.Time) (int64, error)
    IncrementWithinLimit(key string, window time.Time, limit int64) (count int64, allowed bool, err error)
    Get(key string, window time.Time) (int64, error)
    Clear()
    Close() error
}

Built-in constructors:

  • NewMemoryLimiterStore(maxIdle time.Duration) RateLimitStore - In-memory token bucket store with auto-cleanup
  • NewMemoryWindowCounterStore(maxIdle time.Duration) WindowCounterStore - In-memory window counter store with auto-cleanup

Example:

// Use custom store for token bucket rate limiting
customStore := ginx.NewMemoryLimiterStore(30 * time.Minute)
r.Use(ginx.RateLimit(100, 200, ginx.WithStore(customStore)))

// Use custom store for window rate limiting
windowStore := ginx.NewMemoryWindowCounterStore(2 * time.Hour)
r.Use(ginx.RateLimitPerHour(1000, ginx.WithWindowStore(windowStore)))

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 only public GET API responses (requests with Authorization are skipped by default)
        When(ginx.And(ginx.MethodIs("GET"), isAPIPath, ginx.Not(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 via custom middleware)
        When(isAdminData, func(next gin.HandlerFunc) gin.HandlerFunc {
            return func(c *gin.Context) {
                c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
                next(c)
            }
        }).
        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
}

Test Helpers

Ginx exports lightweight test utilities (in test_helpers.go) so downstream packages can unit-test their middleware and handlers without boilerplate.

Context & handler helpers:

  • TestContext(method, path string, headers map[string]string) (*gin.Context, *httptest.ResponseRecorder) - Create a ready-to-use gin.Context in test mode with custom method, path, headers, and a valid RemoteAddr
  • TestMiddleware(name string, executed *[]string) Middleware - Create a middleware that records its execution by appending name to the slice
  • TestHandler(executed *[]string) gin.HandlerFunc - Create a handler that records execution by appending "handler" to the slice

Rate limit helpers:

  • SetupRateLimitTest(t testing.TB) - Register automatic cleanup of global rate limiter state via t.Cleanup; call at the start of any test that exercises rate limiting

Assertion helpers:

  • AssertContains(slice []string, item string) bool - Check if a string slice contains an element
  • AssertEqual(expected, actual interface{}) bool - Check equality of two values
  • AssertSliceEqual(expected, actual []string) bool - Check equality of two string slices

Example:

package myapp

import (
    "testing"
    "github.com/simp-lee/ginx"
)

func TestMyMiddleware(t *testing.T) {
    ginx.SetupRateLimitTest(t) // automatic rate-limiter cleanup

    var executed []string
    c, w := ginx.TestContext("GET", "/api/test", map[string]string{
        "Authorization": "Bearer token123",
    })

    // Build a chain: your middleware + recording handler
    chain := ginx.NewChain().
        Use(ginx.TestMiddleware("before", &executed)).
        Use(MyCustomMiddleware()).
        Build()

    chain(c)

    if w.Code != 200 {
        t.Errorf("expected 200, got %d", w.Code)
    }
    if !ginx.AssertContains(executed, "before") {
        t.Error("expected 'before' middleware to execute")
    }
}

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.11.0 - Web framework
  • golang.org/x/time v0.14.0 - Rate limiting implementation

Feature dependencies (pulled automatically):

  • github.com/simp-lee/jwt - JWT authentication (Auth middleware)
  • github.com/simp-lee/rbac - Role-based access control (RBAC middleware)
  • github.com/simp-lee/logger - Structured logging (Logger/Recovery middleware)
  • github.com/simp-lee/cache - Response caching (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 AbortWithError

func AbortWithError(c *gin.Context, statusCode int, message string)

AbortWithError writes an error response using the ErrorFormatter if set, otherwise falls back to gin.H{"error": message}.

Note: Unlike gin.Context.AbortWithError which takes an error value and appends to ctx.Errors without writing a JSON body, this function writes a JSON response immediately and aborts the chain.

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().

IMPORTANT: This function must only be called after the HTTP server has fully stopped accepting and processing requests. Calling it while requests are still being handled will result in undefined behavior.

Usage:

// At application shutdown (after server.Shutdown())
ginx.CleanupRateLimiters()

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.

Note: This function reads the X-Timeout header from c.Writer, which is the original response writer in outer middleware. Inside a timeout-protected handler, c.Writer is a buffered writer and the X-Timeout header may not be visible. Handlers should use c.Request.Context().Done() to detect their own timeout.

func SetErrorFormatter

func SetErrorFormatter(c *gin.Context, f ErrorFormatter)

SetErrorFormatter sets the ErrorFormatter in the context.

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 SetupRateLimitTest

func SetupRateLimitTest(t testing.TB)

SetupRateLimitTest registers CleanupRateLimiters via t.Cleanup so tests get automatic cleanup of global rate limiter state without manual defer calls. Call this at the start of any test or subtest that exercises rate limiting.

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 AuthConfig

type AuthConfig struct {
	AllowQueryToken bool `json:"allow_query_token"`
}

AuthConfig configures JWT auth token extraction behavior.

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 CacheKeyFunc

type CacheKeyFunc func(*gin.Context) string

CacheKeyFunc generates cache key for request context.

type CacheOption

type CacheOption func(*cacheOptions)

CacheOption configures cache middleware behavior.

func WithCacheKeyFunc

func WithCacheKeyFunc(fn CacheKeyFunc) CacheOption

WithCacheKeyFunc sets custom cache key generation strategy.

func WithCacheVaryHeaders

func WithCacheVaryHeaders(headers ...string) CacheOption

WithCacheVaryHeaders configures request headers that should participate in default cache key generation. Defaults to Accept-Encoding for safe content-encoding separation.

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. The middleware chain is constructed once at setup time, not per request.

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

func (*Chain) WithErrorFormat

func (c *Chain) WithErrorFormat(f ErrorFormatter) *Chain

WithErrorFormat sets the ErrorFormatter for the chain. When set, the formatter is injected into every request context before middleware execution, making it available via GetErrorFormatter.

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. It panics if the pattern is not a valid regular expression. This follows the library's convention of catching configuration errors at init time (like regexp.MustCompile) rather than silently producing always-false conditions at request time.

type ContextInjector

type ContextInjector func(ctx context.Context, requestID string) context.Context

ContextInjector is a function that enriches Go's context.Context with request metadata. This allows attributes like request_id to propagate through the standard context chain and be automatically included in structured logs (e.g., slog with context middleware).

type ErrorFormatter

type ErrorFormatter func(status int, message string) any

ErrorFormatter transforms middleware error responses into a custom format. It receives the HTTP status code and default error message, and returns the response body to be serialized as JSON.

func GetErrorFormatter

func GetErrorFormatter(c *gin.Context) ErrorFormatter

GetErrorFormatter gets the ErrorFormatter from the context. Returns nil if not set.

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. To reduce write lock contention under high concurrency, lastAccess is only updated when more than 1 second has elapsed since the last update. This is safe because the maxIdle duration (typically minutes) is much larger than the 1-second threshold.

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, options ...Option[AuthConfig]) Middleware

Auth is JWT authentication middleware.

func CORS

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

CORS creates a CORS middleware (requires explicit origin configuration).

NOTE: This function panics if AllowCredentials is true and AllowOrigins contains "*", as this is a security violation per the CORS specification. This follows the same pattern as regexp.MustCompile — configuration errors are caught at initialization time rather than producing silent security vulnerabilities at request time.

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 CacheWithGroupOptions

func CacheWithGroupOptions(cache shardedcache.CacheInterface, groupName string, opts ...CacheOption) Middleware

CacheWithGroupOptions creates a cache middleware using specified group and custom options.

func CacheWithOptions

func CacheWithOptions(cache shardedcache.CacheInterface, opts ...CacheOption) Middleware

CacheWithOptions creates a cache middleware using default group and custom options.

func ErrorFormat

func ErrorFormat(f ErrorFormatter) Middleware

ErrorFormat creates a middleware that sets the ErrorFormatter for downstream middleware. Use this when not using Chain, or when you need per-route-group formatting.

func Logger

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

Logger creates a logging middleware with the given options.

NOTE: This function panics if the logger cannot be created (e.g., invalid file path). This follows the same pattern as regexp.MustCompile — configuration errors are caught at initialization time rather than at request time. Callers should ensure valid logger options are provided.

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.

NOTE: This function panics if the logger cannot be created (e.g., invalid file path). This follows the same pattern as regexp.MustCompile — configuration errors are caught at initialization time rather than at request time. Callers should ensure valid logger options are provided.

func RecoveryWith

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

RecoveryWith creates a panic recovery middleware with a custom handler.

NOTE: This function panics if the logger cannot be created (e.g., invalid file path). This follows the same pattern as regexp.MustCompile — configuration errors are caught at initialization time rather than at request time. Callers should ensure valid logger options are provided.

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 executes downstream handlers on an isolated context copy to avoid sharing mutable request execution state between goroutines.

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 WithAuthQueryToken

func WithAuthQueryToken(allow bool) Option[AuthConfig]

WithAuthQueryToken controls whether query token fallback is allowed. Disabled by default for credential leakage prevention.

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 WithMaxBufferSize

func WithMaxBufferSize(size int) Option[TimeoutConfig]

WithMaxBufferSize sets the maximum response buffer size in bytes. When a handler writes a response larger than this limit, the buffered content is flushed directly to the client, bypassing timeout protection for that request. This prevents memory exhaustion from very large response bodies (e.g., file downloads or large database dumps). A value of 0 (default) means unlimited buffering.

func WithTimeout

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

WithTimeout sets timeout duration

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.

Locking contract for implementations: The rate limiter may call Store.Set while holding an internal creation mutex (createMu). Implementations MUST NOT call back into the rate limiter or acquire locks that could be held by callers of Get/Set. The safe pattern is: each store method acquires only its own internal lock, with no external dependencies. The built-in MemoryLimiterStore follows this pattern (createMu → store.mu, always in this order, never reversed).

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 WithTrustedUserHeader

func WithTrustedUserHeader(headerName string) RateOption

WithTrustedUserHeader configures rate limiting by authenticated user ID with explicit trusted-header fallback for deployments behind a trusted gateway.

Security: only use this option when the header is set by trusted infrastructure (e.g., API gateway / auth proxy) and cannot be controlled by external clients.

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 only by 'user_id' in the Gin context (set by auth middleware).

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

	// ContextInjector enriches Go's context.Context with request metadata such as request_id.
	// It is optional and nil by default.
	ContextInjector ContextInjector
}

RequestIDConfig holds configuration for the RequestID middleware

type RequestIDOption

type RequestIDOption = Option[RequestIDConfig]

RequestID options

func WithContextInjector

func WithContextInjector(injector ContextInjector) RequestIDOption

WithContextInjector sets a function that injects the request ID into Go's context.Context. This enables downstream code (service/repository layers) that uses context-aware logging (e.g., slog.InfoContext) to automatically include the request ID in log output.

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 (fixed window)
	TimeWindowHour                     // Per hour (fixed window)
	TimeWindowDay                      // Per day (fixed window)
)

type TimeoutConfig

type TimeoutConfig struct {
	Timeout time.Duration `json:"timeout"` // Timeout duration
	// MaxBufferSize limits the response buffer size in bytes (0 = unlimited).
	// When a response exceeds this limit, it is flushed directly to the client,
	// bypassing timeout protection for that request. This prevents memory exhaustion
	// from very large response bodies while preserving timeout behavior for normal responses.
	MaxBufferSize int `json:"max_buffer_size"`
}

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