Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Exception/ServerException.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public function __construct(array $error = [], ?Throwable $previous = null)
{
$code = $error['code'] ?? 0;
$message = $error['message'] ?? 'Server Error';
$class = $error['data']['class'] ?? '';
if ($class) {
$message = $class . ': ' . $message;
}
Comment on lines +27 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.


$this->error = $error;

Expand Down
17 changes: 13 additions & 4 deletions src/Transporter/MultiplexRpcTransporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,22 @@ public function receive()
{
stream_set_blocking($this->client, false);

$remainingTimeout = $this->timeout;
$start = microtime(true);

while (true) {
$header = $this->readBytes(4);
$header = $this->readBytes(4, $remainingTimeout);
$remainingTimeout = max(0, $remainingTimeout - (microtime(true) - $start));

$unpacked = unpack('Nlength', $header);
$length = $unpacked['length'];

if ($length < 4) {
throw new RecvFailedException(sprintf('Invalid package length: %d', $length));
}
$body = $this->readBytes($length);
$body = $this->readBytes($length, $remainingTimeout);
$remainingTimeout = max(0, $remainingTimeout - (microtime(true) - $start));

if (in_array($body, [self::PING, self::PONG], true)) {
continue;
}
Expand All @@ -47,7 +53,7 @@ public function receive()
/**
* @throws Exception
*/
private function readBytes(int $length): string
private function readBytes(int $length, float $timeout): string
{
$buffer = '';

Expand All @@ -56,7 +62,10 @@ private function readBytes(int $length): string
$write = null;
$except = null;

$selected = stream_select($read, $write, $except, $this->timeout);
$timeoutSeconds = (int) $timeout;
$timeoutMicroseconds = (int) (($timeout - $timeoutSeconds) * 1000000);

$selected = stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds);
Comment on lines +65 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
$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.

if ($selected === false) {
throw new RuntimeException('Failed to select stream.');
}
Expand Down
Loading