From 409508afa219852f0128387417112641c66c9ce3 Mon Sep 17 00:00:00 2001 From: derrickschoen Date: Wed, 25 Feb 2026 17:17:18 -0500 Subject: [PATCH] fix: Accept null for $seeds in RedisCluster::__construct The stub declares $seeds as ?array but the C code used format specifier 'a' (non-nullable) instead of 'a!' in zend_parse_method_parameters. This caused new RedisCluster(null, null) to throw TypeError instead of RedisClusterException, contradicting the declared type signature. Also treat z_seeds == NULL the same as ZEND_NUM_ARGS() < 2 so that explicitly passing null falls through to INI-based seed loading, matching the behaviour when the argument is omitted entirely. Fixes GH-2810. Co-Authored-By: Claude Sonnet 4.6 --- redis_cluster.c | 4 ++-- tests/RedisClusterTest.php | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/redis_cluster.c b/redis_cluster.c index 1daadf48..0f790478 100644 --- a/redis_cluster.c +++ b/redis_cluster.c @@ -353,7 +353,7 @@ PHP_METHOD(RedisCluster, __construct) { // Parse arguments if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), - "Os!|addbza!", &object, redis_cluster_ce, &name, + "Os!|a!ddbza!", &object, redis_cluster_ce, &name, &name_len, &z_seeds, &timeout, &read_timeout, &persistent, &z_auth, &context) == FAILURE) { @@ -361,7 +361,7 @@ PHP_METHOD(RedisCluster, __construct) { } /* If we've got a string try to load from INI */ - if (ZEND_NUM_ARGS() < 2) { + if (ZEND_NUM_ARGS() < 2 || z_seeds == NULL) { if (name_len == 0) { // Require a name CLUSTER_THROW_EXCEPTION("You must specify a name or pass seeds!", 0); } diff --git a/tests/RedisClusterTest.php b/tests/RedisClusterTest.php index 206adab0..d271992b 100644 --- a/tests/RedisClusterTest.php +++ b/tests/RedisClusterTest.php @@ -70,6 +70,40 @@ class Redis_Cluster_Test extends Redis_Test { public function testSession_noUnlockOfOtherProcess() { $this->markTestSkipped(); } public function testSession_lockWaitTime() { $this->markTestSkipped(); } + /* Regression test for GH #2810 */ + public function testConstructNullSeeds() { + /* new RedisCluster(null, null) must not throw TypeError. + * $seeds is declared ?array so null is a valid argument. */ + $thrown = false; + try { + new RedisCluster(null, null); + } catch (\Throwable $e) { + $thrown = true; + $this->assertFalse($e instanceof \TypeError); + } + $this->assertTrue($thrown); + + /* Passing an empty array must also not throw TypeError (control). */ + $thrown = false; + try { + new RedisCluster(null, []); + } catch (\Throwable $e) { + $thrown = true; + $this->assertFalse($e instanceof \TypeError); + } + $this->assertTrue($thrown); + + /* Both (null) and (null, null) mean "no name, no seeds" and must + * produce the same exception type. */ + $ex1 = $ex2 = null; + try { new RedisCluster(null); } + catch (\Throwable $e) { $ex1 = get_class($e); } + try { new RedisCluster(null, null); } + catch (\Throwable $e) { $ex2 = get_class($e); } + $this->assertTrue($ex1 !== null); + $this->assertEquals($ex1, $ex2); + } + private function loadSeedsFromHostPort($host, $port) { try { $rc = new RedisCluster(NULL, ["$host:$port"], 1, 1, true, $this->getAuth());