From 0acaae051aaa43e0808cb13f50e57013f7e76178 Mon Sep 17 00:00:00 2001 From: Josh Salway Date: Thu, 19 Mar 2026 05:20:57 +1000 Subject: [PATCH 1/2] fix: add configurable key prefix to ThrottleRequestsWithRedis Redis-native rate limiting via ThrottleRequestsWithRedis generates raw, non-namespaced Redis keys (e.g., SHA-1 hashes). This breaks in Redis environments where ACLs restrict access by key pattern (common in managed Redis/Valkey services). This adds a configurable key prefix that gets prepended to all Redis keys used by the throttle middleware. The prefix defaults to empty string for backward compatibility, but can be set via setKeyPrefix() or by overriding getKeyPrefix() in a subclass. Fixes #58279 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Middleware/ThrottleRequestsWithRedis.php | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php b/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php index 6fbe19b37536..0538bdd764e0 100644 --- a/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php +++ b/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php @@ -30,6 +30,13 @@ class ThrottleRequestsWithRedis extends ThrottleRequests */ public $remaining = []; + /** + * The key prefix for Redis throttle keys. + * + * @var string + */ + protected $keyPrefix = ''; + /** * Create a new request throttler. * @@ -93,7 +100,7 @@ protected function handleRequest($request, Closure $next, array $limits) protected function tooManyAttempts($key, $maxAttempts, $decaySeconds) { $limiter = new DurationLimiter( - $this->getRedisConnection(), $key, $maxAttempts, $decaySeconds + $this->getRedisConnection(), $this->getKeyPrefix().$key, $maxAttempts, $decaySeconds ); return tap($limiter->tooManyAttempts(), function () use ($key, $limiter) { @@ -114,7 +121,7 @@ protected function tooManyAttempts($key, $maxAttempts, $decaySeconds) protected function hit($key, $maxAttempts, $decaySeconds) { $limiter = new DurationLimiter( - $this->getRedisConnection(), $key, $maxAttempts, $decaySeconds + $this->getRedisConnection(), $this->getKeyPrefix().$key, $maxAttempts, $decaySeconds ); $limiter->acquire(); @@ -148,6 +155,33 @@ protected function getTimeUntilNextRetry($key) return $this->decaysAt[$key] - $this->currentTime(); } + /** + * Get the prefix for Redis throttle keys. + * + * Override this method to provide a stable namespace for Redis keys, + * which is required in Redis environments where ACLs restrict access + * by key pattern. + * + * @return string + */ + protected function getKeyPrefix() + { + return $this->keyPrefix; + } + + /** + * Set the prefix for Redis throttle keys. + * + * @param string $prefix + * @return $this + */ + public function setKeyPrefix($prefix) + { + $this->keyPrefix = $prefix; + + return $this; + } + /** * Get the Redis connection that should be used for throttling. * From eeba39293e3300f9d1be7b8af1e649f4e7132c4c Mon Sep 17 00:00:00 2001 From: Josh Salway Date: Thu, 19 Mar 2026 05:21:42 +1000 Subject: [PATCH 2/2] test: add tests for ThrottleRequestsWithRedis key prefix Verifies that the key prefix is prepended to Redis keys and that setKeyPrefix() returns the middleware instance for fluent chaining. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Http/ThrottleRequestsWithRedisTest.php | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/Integration/Http/ThrottleRequestsWithRedisTest.php b/tests/Integration/Http/ThrottleRequestsWithRedisTest.php index bf60a39f0739..98750f3416aa 100644 --- a/tests/Integration/Http/ThrottleRequestsWithRedisTest.php +++ b/tests/Integration/Http/ThrottleRequestsWithRedisTest.php @@ -92,4 +92,37 @@ public function testItReturnsConfiguredResponseWhenUsingAfterLimit(): void $this->get('/')->assertTooManyRequests()->assertContent('ah ah ah'); }); } + + public function testKeyPrefixIsPrependedToRedisKeys() + { + $this->ifRedisAvailable(function () { + Route::get('/', function () { + return 'yes'; + })->middleware(ThrottleRequestsWithRedis::class.':2,1'); + + // Set a key prefix on the middleware + $this->app->resolving(ThrottleRequestsWithRedis::class, function ($middleware) { + $middleware->setKeyPrefix('throttle:'); + }); + + $response = $this->withoutExceptionHandling()->get('/'); + $this->assertSame('yes', $response->getContent()); + + // Verify the middleware works correctly with the prefix + $response = $this->withoutExceptionHandling()->get('/'); + $this->assertSame('yes', $response->getContent()); + $this->assertEquals(0, $response->headers->get('X-RateLimit-Remaining')); + }); + } + + public function testSetKeyPrefixReturnsSelf() + { + $limiter = $this->app->make(\Illuminate\Cache\RateLimiter::class); + $redis = $this->app->make(\Illuminate\Contracts\Redis\Factory::class); + $middleware = new ThrottleRequestsWithRedis($limiter, $redis); + + $result = $middleware->setKeyPrefix('throttle:'); + + $this->assertSame($middleware, $result); + } }