Skip to content

fix: guard against empty frame data in Swoole onMessage handler#29

Open
claudear wants to merge 3 commits intomainfrom
fix/guard-empty-message-data
Open

fix: guard against empty frame data in Swoole onMessage handler#29
claudear wants to merge 3 commits intomainfrom
fix/guard-empty-message-data

Conversation

@claudear
Copy link

@claudear claudear commented Mar 11, 2026

Summary

  • Guard against empty or null $frame->data in the Swoole adapter's onMessage handler, skipping the callback invocation when there is no meaningful data to process
  • Prevents downstream TypeError when consumers (e.g., Appwrite realtime) try to parse empty WebSocket frame data, which results in null values being passed to typed parameters
  • Adds unit tests for the onMessage behavior with both empty and valid frame data

Context

Fixes Sentry issue CLOUD-3JPZ (650 events).

The error occurs when Swoole receives a WebSocket frame with empty data (e.g., control frames or malformed messages). The onMessage handler passes this empty data to the callback, which then fails when trying to extract fields from the parsed message.

Test plan

  • New unit test verifies callback is NOT invoked for empty frame data
  • New unit test verifies callback IS invoked for valid frame data
  • All existing unit tests pass
  • Lint (pint) passes
  • Static analysis (phpstan level max) passes

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Swoole adapter now correctly ignores frames with empty data, preventing unnecessary callback invocations.
  • Tests

    • Added test suite to verify Swoole adapter properly handles both empty and valid message frames.

Skip invoking the message callback when frame data is empty or null,
preventing downstream errors when consumers try to parse empty messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Warning

Rate limit exceeded

@claudear has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 53 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd20be17-35b8-4b3e-bb45-8083283f2d87

📥 Commits

Reviewing files that changed from the base of the PR and between 7787576 and 99032a6.

📒 Files selected for processing (1)
  • tests/unit/SwooleAdapterTest.php

Walkthrough

This change adds a guard clause to the Swoole adapter's onMessage handler that returns early when a frame contains empty or null data, preventing the callback from being invoked in these cases. Corresponding unit tests are introduced to verify this behavior by mocking the Swoole adapter and Server, using reflection to access private properties, and simulating frame reception to confirm callback invocation patterns for both empty and valid data scenarios.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: guard against empty frame data in Swoole onMessage handler' directly and clearly summarizes the main change: adding a guard to prevent empty frame data from triggering the onMessage callback.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/guard-empty-message-data

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/WebSocket/Adapter/Swoole.php (1)

111-115: Keep empty-message handling consistent across adapters.

Swoole::onMessage() now drops empty payloads, but src/WebSocket/Adapter/Workerman.php:122-135 still forwards empty strings. That makes the shared adapter behavior depend on which backend is selected. Please either mirror this guard in Workerman or document the divergence explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WebSocket/Adapter/Swoole.php` around lines 111 - 115, The Swoole
adapter's message handler now returns early for empty payloads (check using
$frame->data === '' || $frame->data === null) causing inconsistent behavior with
the Workerman adapter; update the Workerman adapter's onMessage handler
(method/class symbol: Workerman::onMessage or its message callback) to perform
the same guard and return early for empty string/null payloads before calling
the shared callback, ensuring both adapters drop empty messages consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/SwooleAdapterTest.php`:
- Around line 12-54: The test testOnMessageSkipsEmptyData currently only covers
an empty-string payload; add an explicit null-payload regression for the same
behavior by reusing the registered callback invocation logic: create a Frame,
set $frame->fd = 1 and $frame->data = null, invoke
$registeredCallback($mockServer, $frame) and assert the $callbackInvoked flag
remains false. Ensure this new assertion sits alongside the existing
empty-string case so both branches of the production guard in Swoole::onMessage
are covered.

---

Nitpick comments:
In `@src/WebSocket/Adapter/Swoole.php`:
- Around line 111-115: The Swoole adapter's message handler now returns early
for empty payloads (check using $frame->data === '' || $frame->data === null)
causing inconsistent behavior with the Workerman adapter; update the Workerman
adapter's onMessage handler (method/class symbol: Workerman::onMessage or its
message callback) to perform the same guard and return early for empty
string/null payloads before calling the shared callback, ensuring both adapters
drop empty messages consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b21bb1d-f80f-4aed-8ff9-38c80f16789e

📥 Commits

Reviewing files that changed from the base of the PR and between d230de8 and 7787576.

📒 Files selected for processing (2)
  • src/WebSocket/Adapter/Swoole.php
  • tests/unit/SwooleAdapterTest.php

Comment on lines +12 to +54
public function testOnMessageSkipsEmptyData(): void
{
$adapter = $this->getMockBuilder(Swoole::class)
->disableOriginalConstructor()
->onlyMethods([])
->getMock();

// Use reflection to access the server property and simulate the onMessage behavior
$reflection = new \ReflectionClass(Swoole::class);
$serverProperty = $reflection->getProperty('server');
$serverProperty->setAccessible(true);

$mockServer = $this->getMockBuilder(Server::class)
->disableOriginalConstructor()
->getMock();

// Capture the callback registered with $server->on('message', ...)
$registeredCallback = null;
$mockServer->expects($this->once())
->method('on')
->with('message', $this->callback(function ($callback) use (&$registeredCallback) {
$registeredCallback = $callback;
return true;
}));

$serverProperty->setValue($adapter, $mockServer);

$callbackInvoked = false;
$adapter->onMessage(function (int $connection, string $message) use (&$callbackInvoked) {
$callbackInvoked = true;
});

$this->assertNotNull($registeredCallback, 'Server on() should have been called');

// Simulate a frame with empty data
$frame = new Frame();
$frame->fd = 1;
$frame->data = '';

$registeredCallback($mockServer, $frame);

$this->assertFalse($callbackInvoked, 'Callback should not be invoked for empty message data');
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a regression case for null frame data.

This suite only exercises the empty-string branch, but the production guard also skips null. Please add an explicit null payload case so both sides of the new condition are covered.

🧰 Tools
🪛 GitHub Actions: Tests

[error] 17-17: During: docker compose exec tests vendor/bin/phpunit --debug -> Error: Call to undefined function enum_exists(). This may indicate the PHP version or environment does not support enums.

🪛 PHPMD (2.15.0)

[warning] 40-40: Avoid unused parameters such as '$connection'. (undefined)

(UnusedFormalParameter)


[warning] 40-40: Avoid unused parameters such as '$message'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/SwooleAdapterTest.php` around lines 12 - 54, The test
testOnMessageSkipsEmptyData currently only covers an empty-string payload; add
an explicit null-payload regression for the same behavior by reusing the
registered callback invocation logic: create a Frame, set $frame->fd = 1 and
$frame->data = null, invoke $registeredCallback($mockServer, $frame) and assert
the $callbackInvoked flag remains false. Ensure this new assertion sits
alongside the existing empty-string case so both branches of the production
guard in Swoole::onMessage are covered.

claudear and others added 2 commits March 11, 2026 13:02
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claudear
Copy link
Author

Fix Confidence: 82/100

The fix correctly guards against empty/null frame data in the Swoole adapter's onMessage handler, which is the entry point shown in the stack trace. The error occurs when downstream code (realtime.php) parses empty data and passes null to getDocument(). By preventing the callback from being called with empty data, we prevent the TypeError. The fix is minimal and all CI checks pass. Confidence is not higher because the actual error occurs in the consumer (realtime.php in appwrite/server-ce) and there could be edge cases where valid but malformed (non-empty) data also triggers the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant