diff --git a/cache_test.go b/cache_test.go index 309e966..4971240 100644 --- a/cache_test.go +++ b/cache_test.go @@ -106,6 +106,36 @@ func TestWithCache_Good_CachesGETResponse(t *testing.T) { } } +func TestWithCacheLimits_Good_CachesGETResponse(t *testing.T) { + gin.SetMode(gin.TestMode) + grp := &cacheCounterGroup{} + e, _ := api.New(api.WithCacheLimits(5*time.Second, 1, 0)) + e.Register(grp) + + h := e.Handler() + + w1 := httptest.NewRecorder() + req1, _ := http.NewRequest(http.MethodGet, "/cache/counter", nil) + h.ServeHTTP(w1, req1) + if w1.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w1.Code) + } + + w2 := httptest.NewRecorder() + req2, _ := http.NewRequest(http.MethodGet, "/cache/counter", nil) + h.ServeHTTP(w2, req2) + if w2.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w2.Code) + } + + if got := w2.Header().Get("X-Cache"); got != "HIT" { + t.Fatalf("expected X-Cache=HIT, got %q", got) + } + if grp.counter.Load() != 1 { + t.Fatalf("expected counter=1 (cached), got %d", grp.counter.Load()) + } +} + func TestWithCache_Good_POSTNotCached(t *testing.T) { gin.SetMode(gin.TestMode) grp := &cacheCounterGroup{} diff --git a/docs/architecture.md b/docs/architecture.md index 8690380..ec0222b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -176,7 +176,8 @@ They execute after `gin.Recovery()` but before any route handler. The `Option` t | `WithBrotli(level...)` | Brotli response compression | Writer pool for efficiency; default compression if level omitted | | `WithSlog(logger)` | Structured request logging | Falls back to `slog.Default()` if nil | | `WithTimeout(d)` | Per-request deadline | 504 with standard error envelope on timeout | -| `WithCache(ttl)` | In-memory GET response caching | `X-Cache: HIT` header on cache hits; 2xx only | +| `WithCache(ttl)` | In-memory GET response caching | Compatibility wrapper for `WithCacheLimits(ttl, 0, 0)`; `X-Cache: HIT` header on cache hits; 2xx only | +| `WithCacheLimits(ttl, maxEntries, maxBytes)` | In-memory GET response caching with explicit bounds | Clearer cache configuration when eviction policy should be self-documenting | | `WithSessions(name, secret)` | Cookie-backed server sessions | gin-contrib/sessions with cookie store | | `WithAuthz(enforcer)` | Casbin policy-based authorisation | Subject from HTTP Basic Auth; 403 on deny | | `WithHTTPSign(secrets, opts...)` | HTTP Signatures verification | draft-cavage-http-signatures; 401/400 on failure | @@ -383,14 +384,19 @@ redirects and introspection). The GraphQL handler is created via gqlgen's ## 8. Response Caching -`WithCache(ttl)` installs a URL-keyed in-memory response cache scoped to GET requests: +`WithCacheLimits(ttl, maxEntries, maxBytes)` installs a URL-keyed in-memory response cache scoped to GET requests: + +```go +engine, _ := api.New(api.WithCacheLimits(5*time.Minute, 100, 10<<20)) +``` - Only successful 2xx responses are cached. - Non-GET methods pass through uncached. - Cached responses are served with an `X-Cache: HIT` header. - Expired entries are evicted lazily on the next access for the same key. - The cache is not shared across `Engine` instances. -- There is no size limit on the cache. +- `WithCache(ttl)` remains available as a compatibility wrapper for callers that do not need to spell out the bounds. +- Passing non-positive values to `WithCacheLimits` leaves that limit unbounded. The implementation uses a `cacheWriter` that wraps `gin.ResponseWriter` to intercept and capture the response body and status code for storage.