package middleware import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "go.uber.org/zap" ) // ResponseCacheConfig configures the HTTP response cache middleware type ResponseCacheConfig struct { RedisClient *redis.Client Logger *zap.Logger DefaultTTL time.Duration KeyPrefix string EndpointTTLs map[string]time.Duration // path prefix → TTL override } // cachedResponse stores the cached HTTP response data type cachedResponse struct { Status int `json:"status"` ContentType string `json:"content_type"` Body string `json:"body"` Headers map[string]string `json:"headers"` } // responseWriter captures the response for caching type cacheResponseWriter struct { gin.ResponseWriter body *bytes.Buffer } func (w *cacheResponseWriter) Write(b []byte) (int, error) { w.body.Write(b) return w.ResponseWriter.Write(b) } // ResponseCache returns a middleware that caches GET responses in Redis. // Only caches successful (2xx) GET requests for unauthenticated or public endpoints. func ResponseCache(cfg ResponseCacheConfig) gin.HandlerFunc { if cfg.RedisClient == nil { return func(c *gin.Context) { c.Next() } } if cfg.DefaultTTL == 0 { cfg.DefaultTTL = 5 * time.Minute } if cfg.KeyPrefix == "" { cfg.KeyPrefix = "http_cache" } if cfg.Logger == nil { cfg.Logger = zap.NewNop() } return func(c *gin.Context) { // Only cache GET requests if c.Request.Method != http.MethodGet { c.Next() return } // Skip caching for authenticated requests (user-specific data) if c.GetHeader("Authorization") != "" { c.Next() return } // Skip caching for cookie-authenticated requests (httpOnly auth cookies) if _, err := c.Cookie("access_token"); err == nil { c.Next() return } // Skip caching for auth endpoints (must never serve cached user data) if strings.Contains(c.Request.URL.Path, "/auth/") { c.Next() return } // Skip caching for binary, range-aware media endpoints. Caching these // strips Accept-Ranges and returns the full body for every request — // the