diff --git a/app/realtime.php b/app/realtime.php index 32c71fa9a3..bd493dc64f 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1104,7 +1104,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.'); } - $unsubscribeResults = []; + // Validate every payload before executing any removal so an invalid entry + // later in the batch does not leave earlier entries half-applied on the server. + $validatedIds = []; foreach ($message['data'] as $payload) { if ( !\is_array($payload) @@ -1114,8 +1116,11 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re ) { throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each unsubscribe payload must include a non-empty subscriptionId.'); } + $validatedIds[] = $payload['subscriptionId']; + } - $subscriptionId = $payload['subscriptionId']; + $unsubscribeResults = []; + foreach ($validatedIds as $subscriptionId) { $wasRemoved = $realtime->unsubscribeSubscription($connection, $subscriptionId); $unsubscribeResults[] = [ 'subscriptionId' => $subscriptionId, diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientQueryTestWithMessage.php b/tests/e2e/Services/Realtime/RealtimeCustomClientQueryTestWithMessage.php index 5dce7056e8..6376875157 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientQueryTestWithMessage.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientQueryTestWithMessage.php @@ -548,6 +548,26 @@ class RealtimeCustomClientQueryTestWithMessage extends Scope $errNonList = $this->sendUnsubscribeMessage($client, ['subscriptionId' => $subB]); $this->assertEquals('error', $errNonList['type']); + // A batch with a valid id followed by an invalid one must be rejected atomically: + // the valid id must remain subscribed, not be quietly removed before validation fails. + $partial = $this->sendUnsubscribeMessage($client, [ + ['subscriptionId' => $subB], + ['subscriptionId' => 999], + ]); + $this->assertEquals('error', $partial['type']); + + $name = 'Partial Rejection Test ' . \uniqid(); + $this->client->call(Client::METHOD_PATCH, '/account/name', \array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => 'a_session_' . $projectId . '=' . $session, + ]), ['name' => $name]); + + $event = \json_decode($client->receive(), true); + $this->assertEquals('event', $event['type']); + $this->assertSame([$subB], $event['data']['subscriptions']); + // Bulk unsubscribe: remaining subB plus a never-existed id -- response mirrors input order $bulk = $this->sendUnsubscribeMessage($client, [ ['subscriptionId' => $subB],