Skip to content

fix: guard unsafe .first access in realtime listeners and query results (#782) - #783

Open
dolliecoder wants to merge 2 commits into
AOSSIE-Org:masterfrom
dolliecoder:fix/safe-first-guard
Open

fix: guard unsafe .first access in realtime listeners and query results (#782)#783
dolliecoder wants to merge 2 commits into
AOSSIE-Org:masterfrom
dolliecoder:fix/safe-first-guard

Conversation

@dolliecoder

@dolliecoder dolliecoder commented Mar 2, 2026

Copy link
Copy Markdown

Description

This PR prevents runtime crashes caused by unsafe .first access on potentially empty lists in realtime listeners and Appwrite query results.

Several controllers were directly accessing:
data.events.first
documents.first

Without checking if the list was empty. If the realtime event list or query result returned an empty list, this would throw:

StateError: No element

Changes made

Added if (data.events.isEmpty) return; before accessing data.events.first
Added isEmpty checks before accessing documents.first
Added safe guards for query result handling to prevent crashes when no documents are returned
These changes ensure the app fails safely instead of crashing in edge cases such as empty realtime events or concurrent deletions.

Fixes #782

Type of change
Bug fix (non-breaking change which fixes an issue)

Checklist

My code follows the style guidelines of this project
I have performed a self-review of my own code
My changes generate no new warnings
I have checked my code and corrected any misspellings

Summary by CodeRabbit

  • Bug Fixes
    • Prevented errors and crashes across messaging, calling, and room features by skipping processing when realtime payloads are empty.
    • Made participant lookup and moderator/speaker actions safer to avoid failures when no matching participant is found.
    • Fixed story unlike and subscriber removal flows to avoid exceptions when expected documents are absent.

@dolliecoder
dolliecoder requested a review from M4dhav as a code owner March 2, 2026 13:01
@github-actions

github-actions Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

🎉 Welcome @dolliecoder!
Thank you for your pull request! Our team will review it soon. 🔍

  • Please ensure your PR follows the contribution guidelines. ✅
  • All automated tests should pass before merging. 🔄
  • If this PR fixes an issue, link it in the description. 🔗

We appreciate your contribution! 🚀

@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Added defensive guards across several controllers to avoid unsafe .first access on realtime event lists and query results. List emptiness is checked before accessing .first; some handlers now return early or handle nulls, and one method's return type was made nullable to reflect absent results. (≤50 words)

Changes

Cohort / File(s) Summary
Realtime listeners (early-return guards)
lib/controllers/friend_calling_controller.dart, lib/controllers/live_chapter_controller.dart, lib/controllers/pair_chat_controller.dart, lib/controllers/room_chat_controller.dart
Each realtime stream listener now returns early when data.events is empty, skipping downstream .first access and payload handling.
Explore story like deletion
lib/controllers/explore_story_controller.dart
unlikeStoryFromUserAccount now checks userLikeDocuments for emptiness before deleting; logs and skips deletion if no matching document found instead of unconditionally using .first.
Single room participant handling
lib/controllers/single_room_controller.dart
Realtime handler added an early-exit for empty data.events. getParticipantDocId signature changed from Future<String> to Future<String?>; call sites updated to handle nullable result and return early when null.
Upcoming rooms subscriber removal
lib/controllers/upcomming_rooms_controller.dart
removeUserFromSubscriberList now captures query result, checks for empty result.documents and logs/returns early if empty before using .first and deleting the subscription document.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I nibble code with gentle care,
I check each list—no empty snare.
.first no longer makes me sigh,
I hop on, safe, and wink an eye.
A little guard, a happier sky. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: guarding unsafe .first access in realtime listeners and query results, which matches the core objective of preventing crashes from empty lists.
Linked Issues check ✅ Passed All code changes directly address issue #782 requirements: guards added before .first access on data.events across realtime listeners, isEmpty checks added before accessing documents.first in query results, and null-safety improvements for participantDocId lookups.
Out of Scope Changes check ✅ Passed All changes are scoped to the linked issue: guards on realtime event streams, query result checks, and error handling improvements. The null-safety update to getParticipantDocId and its call-site handling are defensive improvements directly supporting the safe-first-guard objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/controllers/explore_story_controller.dart (1)

629-649: ⚠️ Potential issue | 🟠 Major

Only decrement story likes when a like document was actually removed.

When Line 640 path is hit (no like doc found), the code still decrements likes at Line 648. This can corrupt like counts in concurrent/unlike-twice scenarios.

Suggested fix
-    if (userLikeDocuments.isNotEmpty) {
+    bool removedLike = false;
+    if (userLikeDocuments.isNotEmpty) {
       try {
         await databases.deleteDocument(
           databaseId: storyDatabaseId,
           collectionId: likeCollectionId,
           documentId: userLikeDocuments.first.$id,
         );
+        removedLike = true;
       } on AppwriteException catch (e) {
         log('Failed to Unlike i.e delete Like Document: ${e.message}');
       }
     } else {
       log('No like document found to delete');
     }

-    try {
-      await databases.updateDocument(
-        databaseId: storyDatabaseId,
-        collectionId: storyCollectionId,
-        documentId: story.storyId,
-        data: {"likes": story.likesCount.value - 1},
-      );
-    } on AppwriteException catch (e) {
-      log("Failed to reduce one story like: ${e.message}");
-    }
+    if (removedLike) {
+      try {
+        await databases.updateDocument(
+          databaseId: storyDatabaseId,
+          collectionId: storyCollectionId,
+          documentId: story.storyId,
+          data: {"likes": story.likesCount.value - 1},
+        );
+      } on AppwriteException catch (e) {
+        log("Failed to reduce one story like: ${e.message}");
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/controllers/explore_story_controller.dart` around lines 629 - 649, The
code currently always decrements likes via databases.updateDocument even when no
like document was found; modify the flow so that the decrement only happens when
a like document was actually removed: check userLikeDocuments.isNotEmpty and
perform databases.deleteDocument, and only if that deletion completes
successfully (no AppwriteException) call databases.updateDocument to decrement
the story.likesCount for story.storyId; alternatively set a boolean (e.g.,
deletedLike) to true upon successful deletion and wrap the updateDocument call
in an if (deletedLike) block to avoid decrementing on the "No like document
found" or deletion failure paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/controllers/single_room_controller.dart`:
- Around line 248-250: Change getParticipantDocId's return type from
Future<String> to Future<String?> so it can legally return null when
participantDocsRef.documents.isEmpty; then update all call sites in
makeModerator, removeModerator, makeSpeaker, makeListener, and
kickOutParticipant to handle a nullable result by checking if participantDocId
== null and returning/aborting early (e.g., if (participantDocId == null)
return;) before using participantDocId.

In `@lib/controllers/upcomming_rooms_controller.dart`:
- Around line 119-124: The anonymous .then callback currently throws a generic
Exception when value.documents.isEmpty which escapes since only
AppwriteException is caught; instead, remove the throw and return a safe empty
value (e.g., null or an empty Map/Object matching the expected document type)
from the callback where value.documents.first is returned so callers can handle
an empty result; update the enclosing function's return type if necessary and
adjust callers to handle the null/empty response.

---

Outside diff comments:
In `@lib/controllers/explore_story_controller.dart`:
- Around line 629-649: The code currently always decrements likes via
databases.updateDocument even when no like document was found; modify the flow
so that the decrement only happens when a like document was actually removed:
check userLikeDocuments.isNotEmpty and perform databases.deleteDocument, and
only if that deletion completes successfully (no AppwriteException) call
databases.updateDocument to decrement the story.likesCount for story.storyId;
alternatively set a boolean (e.g., deletedLike) to true upon successful deletion
and wrap the updateDocument call in an if (deletedLike) block to avoid
decrementing on the "No like document found" or deletion failure paths.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf1dbe2 and e1b6ef2.

📒 Files selected for processing (7)
  • lib/controllers/explore_story_controller.dart
  • lib/controllers/friend_calling_controller.dart
  • lib/controllers/live_chapter_controller.dart
  • lib/controllers/pair_chat_controller.dart
  • lib/controllers/room_chat_controller.dart
  • lib/controllers/single_room_controller.dart
  • lib/controllers/upcomming_rooms_controller.dart

Comment thread lib/controllers/single_room_controller.dart
Comment thread lib/controllers/upcomming_rooms_controller.dart Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
lib/controllers/single_room_controller.dart (1)

296-297: Consider extracting repeated nullable lookup guard into a helper.

The null handling is correct, but this pattern is duplicated across five methods; extracting it will reduce drift risk.

Refactor sketch
+  Future<void> _withParticipantDocId(
+    Participant participant,
+    Future<void> Function(String participantDocId) action,
+  ) async {
+    final participantDocId = await getParticipantDocId(participant);
+    if (participantDocId == null) return;
+    await action(participantDocId);
+  }

   Future<void> makeModerator(Participant participant) async {
-    final participantDocId = await getParticipantDocId(participant);
-    if (participantDocId == null) return;
-    await updateParticipantDoc(participantDocId, {
+    await _withParticipantDocId(participant, (participantDocId) async {
+      await updateParticipantDoc(participantDocId, {
         "isSpeaker": true,
         "hasRequestedToBeSpeaker": false,
         "isModerator": true,
-    });
+      });
+    });
   }

Also applies to: 306-307, 341-342, 350-351, 359-360

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

In `@lib/controllers/single_room_controller.dart` around lines 296 - 297, Multiple
methods in single_room_controller.dart repeatedly perform the nullable lookup
pattern: calling getParticipantDocId(participant) and returning when null;
extract this into a small helper (e.g., requireParticipantDocId or
withParticipantDocId) that encapsulates the lookup and null-guard so callers
pass the participant and either receive a non-null docId or execute a callback
only when docId != null; update the five occurrences that call
getParticipantDocId (the lines around the calls to getParticipantDocId in the
methods that currently do "final participantDocId = await
getParticipantDocId(participant); if (participantDocId == null) return;") to use
the new helper to eliminate duplicated nullable-guard logic while preserving the
original early-return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/controllers/single_room_controller.dart`:
- Around line 296-297: Multiple methods in single_room_controller.dart
repeatedly perform the nullable lookup pattern: calling
getParticipantDocId(participant) and returning when null; extract this into a
small helper (e.g., requireParticipantDocId or withParticipantDocId) that
encapsulates the lookup and null-guard so callers pass the participant and
either receive a non-null docId or execute a callback only when docId != null;
update the five occurrences that call getParticipantDocId (the lines around the
calls to getParticipantDocId in the methods that currently do "final
participantDocId = await getParticipantDocId(participant); if (participantDocId
== null) return;") to use the new helper to eliminate duplicated nullable-guard
logic while preserving the original early-return behavior.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1b6ef2 and 2095ee1.

📒 Files selected for processing (2)
  • lib/controllers/single_room_controller.dart
  • lib/controllers/upcomming_rooms_controller.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/controllers/upcomming_rooms_controller.dart

@dolliecoder

Copy link
Copy Markdown
Author

@M4dhav please review the pr once

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.

Prevent runtime crashes from unsafe .first access on realtime events and query results

1 participant