Compare commits

..

1 Commits

Author SHA1 Message Date
Sergey Sannikov 3ffaf10e99 Return NAN for RESP3 NaN double payloads (#1718)
parseDouble() fell through to a float cast for ',nan', and
(float) 'nan' evaluates to 0.0 in PHP, so a NaN score silently became
a valid zero. Return NAN instead. The RESP3 specification also notes
that Redis before 7.2 may emit any libc representation of NaN
('-nan', 'NAN', 'nan(char-sequence)') and that clients should handle
them, so those spellings are accepted as well.

The TDigest RESP3 tests asserted 0 or null for empty sketches, which
only passed because of the collapsed 0.0 (null == 0.0 loosely); their
RESP2 counterparts already assert the string 'nan' for the same
replies. They now assert NaN.

Note for reviewers: json_encode() throws on NAN, so consumers who
serialize raw replies must handle it; the previous behavior hid NaN
behind a plausible-looking 0.0 instead.
2026-09-10 15:29:52 -07:00
10 changed files with 55 additions and 14 deletions
+1 -4
View File
@@ -1,11 +1,8 @@
## Changelog
## Unreleased
### Added
### Changed
### Fixed
- Fixed RESP3 double parsing returning positive `INF` for `-inf` payloads (#1716)
- Fixed `client_info` connection parameter being ignored (#1722)
- Changed RESP3 double parsing to return `NAN` for NaN payloads instead of `0.0`
## v3.6.0 (2026-08-14)
### Added
@@ -71,6 +71,10 @@ class Resp3Strategy extends Resp2Strategy
return -INF;
}
if (preg_match('/^-?nan(\(.*\))?$/i', $string) === 1) {
return NAN;
}
return (float) $string;
}
@@ -118,7 +118,11 @@ class TDIGESTBYRANK_Test extends PredisCommandTestCase
$actualResponse = $redis->tdigestbyrank('key', 0, 1, 2, 3, 4, 5, 6);
$this->assertEquals($expectedResponse, $actualResponse);
$this->assertEquals([null, null], $redis->tdigestbyrank('empty_key', 0, 1));
$emptyResponse = $redis->tdigestbyrank('empty_key', 0, 1);
$this->assertCount(2, $emptyResponse);
foreach ($emptyResponse as $value) {
$this->assertNan($value);
}
}
/**
@@ -118,7 +118,11 @@ class TDIGESTBYREVRANK_Test extends PredisCommandTestCase
$actualResponse = $redis->tdigestbyrevrank('key', 0, 1, 2, 3, 4, 5, 6);
$this->assertEquals($expectedResponse, $actualResponse);
$this->assertEquals([null, null], $redis->tdigestbyrevrank('empty_key', 0, 1));
$emptyResponse = $redis->tdigestbyrevrank('empty_key', 0, 1);
$this->assertCount(2, $emptyResponse);
foreach ($emptyResponse as $value) {
$this->assertNan($value);
}
}
/**
@@ -118,7 +118,11 @@ class TDIGESTCDF_Test extends PredisCommandTestCase
$actualResponse = $redis->tdigestcdf('key', 0, 1, 2, 3, 4);
$this->assertSameWithPrecision($expectedResponse, $actualResponse, 5);
$this->assertSame([0.0, 0.0], $redis->tdigestcdf('empty_key', 0, 1));
$emptyResponse = $redis->tdigestcdf('empty_key', 0, 1);
$this->assertCount(2, $emptyResponse);
foreach ($emptyResponse as $value) {
$this->assertNan($value);
}
}
/**
@@ -116,7 +116,7 @@ class TDIGESTMAX_Test extends PredisCommandTestCase
$actualResponse = $redis->tdigestmax('key');
$this->assertEquals('5', $actualResponse);
$this->assertEquals(0, $redis->tdigestmax('empty_key'));
$this->assertNan($redis->tdigestmax('empty_key'));
}
/**
@@ -116,7 +116,7 @@ class TDIGESTMIN_Test extends PredisCommandTestCase
$actualResponse = $redis->tdigestmin('key');
$this->assertEquals('1', $actualResponse);
$this->assertEquals(0, $redis->tdigestmin('empty_key'));
$this->assertNan($redis->tdigestmin('empty_key'));
}
/**
@@ -118,7 +118,11 @@ class TDIGESTQUANTILE_Test extends PredisCommandTestCase
$this->assertEquals([1.0, 2.0, 3.0, 3.0, 4.0, 4.0, 4.0, 5.0, 5.0, 5.0, 5.0], $quantileResponse);
$redis->tdigestcreate('empty_key');
$this->assertEquals([null, null], $redis->tdigestquantile('empty_key', 0.0, 0.1));
$emptyResponse = $redis->tdigestquantile('empty_key', 0.0, 0.1);
$this->assertCount(2, $emptyResponse);
foreach ($emptyResponse as $value) {
$this->assertNan($value);
}
}
/**
@@ -129,10 +129,11 @@ class TDIGESTRESET_Test extends PredisCommandTestCase
$this->assertEquals('OK', $actualResponse);
$this->assertSame(500, $info['Compression']);
$this->assertEquals(
[null, null, null, null, null, null],
$redis->tdigestbyrank('key', 0, 1, 2, 3, 4, 5)
);
$resetResponse = $redis->tdigestbyrank('key', 0, 1, 2, 3, 4, 5);
$this->assertCount(6, $resetResponse);
foreach ($resetResponse as $value) {
$this->assertNan($value);
}
}
/**
@@ -67,6 +67,19 @@ class Resp3StrategyTest extends PredisTestCase
$this->assertSame($expectedValue, $actualResponse);
}
/**
* @dataProvider nanProvider
* @group disconnected
* @param string $data
* @return void
*/
public function testParseDataReturnsFloatNanOnNanValue(string $data): void
{
$actualResponse = $this->strategy->parseData($data);
$this->assertNan($actualResponse);
}
/**
* @dataProvider booleanProvider
* @group disconnected
@@ -171,6 +184,16 @@ class Resp3StrategyTest extends PredisTestCase
];
}
public function nanProvider(): array
{
return [
'canonical nan' => [",nan\r\n"],
'negative nan' => [",-nan\r\n"],
'uppercase nan' => [",NAN\r\n"],
'nan with payload' => [",nan(ind)\r\n"],
];
}
public function booleanProvider(): array
{
return [