MultiplexRpcTransporter strict timeout . - #43
Conversation
📝 WalkthroughWalkthroughThe changes add optional server class prefixes to exception messages and update multiplexed RPC reads to track and apply the remaining configured timeout across header and body reads. ChangesServer exception messages
Multiplexed RPC read timeout
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 PHPStan (2.2.5)PHPStan was skipped because the user-provided config is missing the required Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Exception/ServerException.php`:
- Around line 27-30: Update the exception message construction in
ServerException to extract data before accessing class, verify data is an array,
and only prepend class when it is a non-empty string. Preserve the existing
fallback message when data or class is invalid.
In `@src/Transporter/MultiplexRpcTransporter.php`:
- Around line 65-68: Update readBytes() to compute a single deadline when the
read begins, then recalculate the remaining seconds and microseconds before
every stream_select() iteration, including after partial fread() results. Ensure
the configured timeout is not reset between reads and add a regression test
covering trickled partial data that verifies receive() respects the overall
deadline.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b969db55-0ee4-4688-919f-04cc8bed5cb8
📒 Files selected for processing (2)
src/Exception/ServerException.phpsrc/Transporter/MultiplexRpcTransporter.php
| $class = $error['data']['class'] ?? ''; | ||
| if ($class) { | ||
| $message = $class . ': ' . $message; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
php -r '
$cases = [
["message" => "failure", "data" => "malformed"],
["message" => "failure", "data" => ["class" => ["unexpected"]]],
];
foreach ($cases as $error) {
try {
$class = $error["data"]["class"] ?? "";
echo $class . ": " . $error["message"] . PHP_EOL;
} catch (Throwable $e) {
echo get_class($e) . ": " . $e->getMessage() . PHP_EOL;
}
}
'Repository: friendsofhyperf/jet
Length of output: 255
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== file =="
cat -n src/Exception/ServerException.php | sed -n '1,80p'
echo "== composer / PHP versions =="
if [ -f composer.json ]; then
python3 - <<'PY'
import json
p=json.load(open('composer.json'))
print(json.dumps(p.get('require', {}).get('php'),'ignore'), indent=2)
print('composer require php:', p.get('require', {}).get('php'))
PY
fi
echo "== usage of ServerException =="
rg -n "new ServerException|ServerException::|data\]\s*=>|class\]\s*=>|server.*error" src -S || true
echo "== behavioral PHP probe =="
php -r '
error_reporting(E_ALL);
set_error_handler(function($severity, $message, $file, $line) {
throw new \ErrorException($message, 0, $severity, $file, $line);
});
$cases = [
["message" => "failure", "data" => "malformed"],
["message" => "failure", "data" => ["class" => ["unexpected"]]],
["message" => "failure", "data" => ["class" => ["array"]], "data" => ["class" => ["array"]]],
];
try {
php -r '"'"'
set_error_handler(function($severity, $message, $file, $line) { throw new Exception("error",$severity); });
$cases = [
["message"=> "failure", "data"=> "malformed"],
["message"=> "failure", "data"=> ["class"=> ["unexpected"]]],
["message"=> "failure", "data"=> ["class"=> 123]],
];
foreach ($cases as $error) {
try {
$class = $error["data"]["class"] ?? "";
echo $class . ": " . $error["message"] . PHP_EOL;
} catch (Throwable $e) {
echo get_class($e) . ": " . $e->getMessage() . PHP_EOL;
}
}
'"'"'
except (Throwable $e) {
echo get_class($e) . ": " . $e->getMessage() . " at line " . $e->getLine() . PHP_EOL;
}
'Repository: friendsofhyperf/jet
Length of output: 1566
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== composer dependencies =="
if [ -f composer.json ]; then
python3 - <<'PY'
import json
p=json.load(open('composer.json'))
print("php:", p.get('require', {}).get('php'))
print("require:", p.get('require', {}))
PY
fi
echo "== relevant usage/tests mentions =="
rg -n "ServerException|rpc|error|data\]\s*=>" . -S --glob '!vendor/**' --glob '!node_modules/**' | head -n 200
echo "== PHP runtime probe with type conversion/strictness =="
php -r '
error_reporting(E_ALL);
set_error_handler(function($severity, $message, $file, $line) {
if (PHP_VERSION_ID >= 80400 && in_array($severity, [E_WARNING, E_DEPRECATED], true)) {
throw new ErrorException($message, 0, $severity, $file, $line);
}
throw new Exception($message, $severity);
});
$cases = [
["message" => "failure", "data" => "malformed"],
["message" => "failure", "data" => ["class" => ["unexpected"]]],
["message" => "failure", "data" => ["class" => 123]],
];
foreach ($cases as $error) {
try {
$class = $error["data"]["class"] ?? "";
echo get_class(new \Exception($class . ": " . $error["message"])) . PHP_EOL;
} catch (Throwable $e) {
echo "[" . get_class($e) . "] " . $e->getMessage() . " at line " . $e->getLine() . PHP_EOL;
}
}
'Repository: friendsofhyperf/jet
Length of output: 6493
Validate data['class'] before using it in the exception message.
?? only provides a default for missing keys; it does not prevent offset access on non-array data. Also, non-string class values still enter the concatenation and can fire conversion warnings/errors during ServerException construction. Extract data first, check it is an array, and only use non-empty string class values.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Exception/ServerException.php` around lines 27 - 30, Update the exception
message construction in ServerException to extract data before accessing class,
verify data is an array, and only prepend class when it is a non-empty string.
Preserve the existing fallback message when data or class is invalid.
| $timeoutSeconds = (int) $timeout; | ||
| $timeoutMicroseconds = (int) (($timeout - $timeoutSeconds) * 1000000); | ||
|
|
||
| $selected = stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Preserve the deadline across partial reads.
$timeout is reused for every stream_select() call inside readBytes(). If fread() returns partial data, a peer can repeatedly reset the full wait period and keep receive() blocked beyond the configured timeout. Compute a deadline once per readBytes() call and pass the remaining duration on each iteration; add a regression test for trickled partial reads.
Proposed fix
private function readBytes(int $length, float $timeout): string
{
$buffer = '';
+ $deadline = microtime(true) + $timeout;
while (strlen($buffer) < $length) {
$read = [$this->client];
$write = null;
$except = null;
- $timeoutSeconds = (int) $timeout;
- $timeoutMicroseconds = (int) (($timeout - $timeoutSeconds) * 1000000);
+ $remaining = $deadline - microtime(true);
+ if ($remaining <= 0) {
+ throw new RecvFailedException('Receive timeout.');
+ }
+ $timeoutSeconds = (int) $remaining;
+ $timeoutMicroseconds = (int) (($remaining - $timeoutSeconds) * 1000000);
$selected = stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $timeoutSeconds = (int) $timeout; | |
| $timeoutMicroseconds = (int) (($timeout - $timeoutSeconds) * 1000000); | |
| $selected = stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds); | |
| $deadline = microtime(true) + $timeout; | |
| $remaining = $deadline - microtime(true); | |
| if ($remaining <= 0) { | |
| throw new RecvFailedException('Receive timeout.'); | |
| } | |
| $timeoutSeconds = (int) $remaining; | |
| $timeoutMicroseconds = (int) (($remaining - $timeoutSeconds) * 1000000); | |
| $selected = stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Transporter/MultiplexRpcTransporter.php` around lines 65 - 68, Update
readBytes() to compute a single deadline when the read begins, then recalculate
the remaining seconds and microseconds before every stream_select() iteration,
including after partial fread() results. Ensure the configured timeout is not
reset between reads and add a regression test covering trickled partial data
that verifies receive() respects the overall deadline.
Summary by CodeRabbit