diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f78c9ee..d296a0bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Fixed Relay returning server errors as error responses and raw error messages without stack traces (#1292) - Fixed Relay reconnecting after an explicit disconnect (#1292) - Fixed Relay ACL authentication accepting username and password (#1292) +- Fixed Relay throwing a `TypeError` instead of returning an error response when selecting a non-integer database (#1751) - Fixed `INFO` parsing warnings with Valkey 9 (#1748) ## v3.7.0 (2026-10-08) diff --git a/src/Connection/RelayConnection.php b/src/Connection/RelayConnection.php index 5d4d462d..ecba1988 100644 --- a/src/Connection/RelayConnection.php +++ b/src/Connection/RelayConnection.php @@ -12,10 +12,13 @@ namespace Predis\Connection; +use TypeError; +use ArgumentCountError; use InvalidArgumentException; use Predis\ClientException; use Predis\Command\CommandInterface; use Predis\NotSupportedException; +use Predis\Response\Error as ErrorResponse; use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\ServerException; use Relay\Exception as RelayException; @@ -204,6 +207,10 @@ class RelayConnection extends AbstractConnection $arguments = [$arguments]; } + if ($name === 'SELECT') { + return $this->select($arguments); + } + // Raw dispatch leaves values untouched. Serialization and compression // are applied explicitly with pack() and unpack(). return in_array($name, $this->atypicalCommands) @@ -222,6 +229,24 @@ class RelayConnection extends AbstractConnection } } + /** + * Relay's select() throws a TypeError for a non-integer database, where + * other connections get an error response from the server. + * + * @param array $arguments + * @return mixed + */ + protected function select(array $arguments) + { + try { + return $this->client->select(...$arguments); + } catch (ArgumentCountError $ex) { + throw $ex; + } catch (TypeError $ex) { + return new ErrorResponse('ERR value is not an integer or out of range'); + } + } + /** * {@inheritdoc} */ diff --git a/tests/Predis/Command/Redis/SELECT_Test.php b/tests/Predis/Command/Redis/SELECT_Test.php index 551c72c7..76e223c0 100644 --- a/tests/Predis/Command/Redis/SELECT_Test.php +++ b/tests/Predis/Command/Redis/SELECT_Test.php @@ -83,9 +83,42 @@ class SELECT_Test extends PredisCommandTestCase $redis->select(100000000); } + /** + * Relay warns before sending SELECT for indexes beyond `relay.databases`, + * which PHPUnit would turn into an exception before the server replies. + * + * @group connected + */ + public function testThrowsExceptionOnUnexpectedDatabaseRangeDespiteRelayWarning(): void + { + if (!getenv('USE_RELAY')) { + $this->markTestSkipped('Relay only warns when it is the active connection.'); + } + + $this->expectException('Predis\Response\ServerException'); + $this->expectExceptionMessageMatches('/ERR.*DB index/'); + + $redis = $this->getClient(); + + $previous = set_error_handler( + static function ($level, $message, $file = '', $line = 0) use (&$previous) { + if (strpos($message, 'in-memory databases') !== false) { + return true; + } + + return $previous ? $previous($level, $message, $file, $line) : false; + } + ); + + try { + $redis->select(100000000); + } finally { + restore_error_handler(); + } + } + /** * @group connected - * @group relay-incompatible */ public function testThrowsExceptionOnUnexpectedDatabaseName(): void { diff --git a/tests/Predis/Connection/RelayConnectionTest.php b/tests/Predis/Connection/RelayConnectionTest.php index 309e48cb..7c4b1b3a 100644 --- a/tests/Predis/Connection/RelayConnectionTest.php +++ b/tests/Predis/Connection/RelayConnectionTest.php @@ -12,6 +12,7 @@ namespace Predis\Connection; +use ArgumentCountError; use PHPUnit\Framework\MockObject\MockObject; use Predis\Client; use Predis\ClientException; @@ -24,6 +25,7 @@ use PredisTestCase; use ReflectionClass; use Relay\Exception as RelayException; use Relay\Relay; +use TypeError; /** * @group ext-relay @@ -166,6 +168,72 @@ class RelayConnectionTest extends PredisTestCase $this->assertTrue($response); } + /** + * @group disconnected + * @return void + */ + public function testExecuteSelectOnClient(): void + { + $this->mockClient + ->expects($this->once()) + ->method('isConnected') + ->withAnyParameters() + ->willReturn(true); + + $this->mockClient + ->expects($this->once()) + ->method('SELECT') + ->with(1) + ->willReturn(true); + + $response = $this->connection->executeCommand(new RawCommand('SELECT', [1])); + + $this->assertTrue($response); + } + + /** + * @group disconnected + * @return void + */ + public function testExecuteSelectReturnsErrorResponseOnNonIntegerDatabase(): void + { + $this->mockClient + ->expects($this->once()) + ->method('isConnected') + ->withAnyParameters() + ->willReturn(true); + + $this->mockClient + ->method('SELECT') + ->willThrowException(new TypeError('Argument #1 ($db) must be of type int, string given')); + + $response = $this->connection->executeCommand(new RawCommand('SELECT', ['x'])); + + $this->assertInstanceOf(ErrorResponse::class, $response); + $this->assertSame('ERR value is not an integer or out of range', (string) $response); + } + + /** + * @group disconnected + * @return void + */ + public function testExecuteSelectDoesNotHideArgumentCountError(): void + { + $this->mockClient + ->expects($this->once()) + ->method('isConnected') + ->withAnyParameters() + ->willReturn(true); + + $this->mockClient + ->method('SELECT') + ->willThrowException(new ArgumentCountError('Too few arguments')); + + $this->expectException(ArgumentCountError::class); + + $this->connection->executeCommand(new RawCommand('SELECT', [])); + } + /** * @group disconnected * @return void