Description
src/lib/cache.ts:
- Lines 281-319
pickDefaultBackend(): if neither KV_REST_API_URL+KV_REST_API_TOKEN nor REDIS_URL is set, it returns MemoryBackend — and in production only logs console.error (lines 310-317) before continuing.
- Lines 290-301: even when Redis is configured, the ioredis client uses
maxRetriesPerRequest: 1 and retryStrategy: () => null, and on the first connection error permanently swaps the singleton backend = new MemoryBackend() (line 298) — the instance never recovers.
- Lines 322-326:
isSharedCacheAvailable() is only used to print a warning.
src/lib/rate-limit.ts:40-53 then delegates all sliding-window counting to whatever backend is live. Consumers:
- Webhook receiver
src/app/api/webhooks/github/route.ts:58-67 (100/min per install, the only webhook DoS guard)
- Every authenticated server action via
src/lib/action-auth.ts:41-51
- Dead-letter retry
src/app/api/webhooks/github/retry/route.ts:68-77 (10/min)
Expected Behavior
On a serverless platform (each invocation is an isolated process), rate-limit state must be shared and durable; a misconfiguration or Redis outage must fail closed, not silently pass.
Actual Behavior
With MemoryBackend, counters live in a per-invocation map. Rate limits are effectively disabled — a single user can hammer the webhook endpoint, retry endpoint, or any action without being throttled, because each serverless instance resets its own map. Worse, the failure is silent (console log only), so the webhook 429 protection and all per-user action throttles can be bypassed during exactly the deployment scenarios (forgotten KV vars, Redis blip) where they matter most.
Affected Files
src/lib/cache.ts (lines 281-319, 290-301, 322-326) — Memory fallback + permanent swap on Redis error
src/lib/rate-limit.ts (lines 40-53) — Delegates to whichever backend is live
src/app/api/webhooks/github/route.ts (lines 58-67) — Webhook rate limit
src/lib/action-auth.ts (lines 41-51) — Action rate limit
src/app/api/webhooks/github/retry/route.ts (lines 68-77) — Dead-letter retry limit
Proposed Fix
- Make rate-limiting fail closed: if
NODE_ENV === 'production' and no distributed backend is available, rateLimit() should return ok: false (block) rather than pass
- Remove the permanent backend swap on Redis error; add a reconnect/health-check path that re-attempts Redis
- Treat missing KV in production as a boot/deploy failure (fail the middleware gate or surface a prominent startup error), not a degraded runtime state
Description
src/lib/cache.ts:pickDefaultBackend(): if neitherKV_REST_API_URL+KV_REST_API_TOKENnorREDIS_URLis set, it returnsMemoryBackend— and in production only logsconsole.error(lines 310-317) before continuing.maxRetriesPerRequest: 1andretryStrategy: () => null, and on the first connection error permanently swaps the singletonbackend = new MemoryBackend()(line 298) — the instance never recovers.isSharedCacheAvailable()is only used to print a warning.src/lib/rate-limit.ts:40-53then delegates all sliding-window counting to whatever backend is live. Consumers:src/app/api/webhooks/github/route.ts:58-67(100/min per install, the only webhook DoS guard)src/lib/action-auth.ts:41-51src/app/api/webhooks/github/retry/route.ts:68-77(10/min)Expected Behavior
On a serverless platform (each invocation is an isolated process), rate-limit state must be shared and durable; a misconfiguration or Redis outage must fail closed, not silently pass.
Actual Behavior
With
MemoryBackend, counters live in a per-invocation map. Rate limits are effectively disabled — a single user can hammer the webhook endpoint, retry endpoint, or any action without being throttled, because each serverless instance resets its own map. Worse, the failure is silent (console log only), so the webhook 429 protection and all per-user action throttles can be bypassed during exactly the deployment scenarios (forgotten KV vars, Redis blip) where they matter most.Affected Files
src/lib/cache.ts(lines 281-319, 290-301, 322-326) — Memory fallback + permanent swap on Redis errorsrc/lib/rate-limit.ts(lines 40-53) — Delegates to whichever backend is livesrc/app/api/webhooks/github/route.ts(lines 58-67) — Webhook rate limitsrc/lib/action-auth.ts(lines 41-51) — Action rate limitsrc/app/api/webhooks/github/retry/route.ts(lines 68-77) — Dead-letter retry limitProposed Fix
NODE_ENV === 'production'and no distributed backend is available,rateLimit()should returnok: false(block) rather than pass