Skip to content

RTDB: getData never completes if the connection drops after the get is sent #16717

Description

@nickcernera

Description

A getDataWithCompletionBlock: whose request has already been sent when the websocket disconnects never calls its completion block. The call neither returns data nor fails.

From FirebaseDatabase/Sources/Core/FPersistentConnection.m (12.8.0, unchanged on main):

  1. sendGet: sets get.sent = YES and registers the response callback in requestCBHash.
  2. onDisconnect:withReason: clears requestCBHash, which drops that callback. It neither fails outstanding gets nor resets their sent flag.
  3. On reconnect, restoreState calls sendGet: for every entry in outstandingGets, but sendGet: begins with if ([get sent]) { return; }. The get is never resent, and its completion never fires.

The connect timeout in getDataAtPath:withParams:withCallback: is armed only when the client is disconnected at call time, so it does not cover this case.

Android resends such gets: PersistentConnectionImpl.restoreState resends every outstanding get. (Its own early return for a get already sent sits inside if (logger.logsDebug()), so with debug logging off the get falls through and is resent. That looks accidental, but it gives the behaviour you'd want.)

Proposed fix

Mark outstanding gets unsent on disconnect so restoreState resends them, matching Android:

diff --git a/FirebaseDatabase/Sources/Core/FPersistentConnection.m b/FirebaseDatabase/Sources/Core/FPersistentConnection.m
index da2820d..40dfaa7 100644
--- a/FirebaseDatabase/Sources/Core/FPersistentConnection.m
+++ b/FirebaseDatabase/Sources/Core/FPersistentConnection.m
@@ -400,6 +400,15 @@ - (void)onDisconnect:(FConnection *)fconnection
     self.realtime = nil;
     [self cancelSentTransactions];
     [self.requestCBHash removeAllObjects];
+    // the line above drops the response callback of every
+    // get already sent, and restoreState's sendGet: skips a get marked sent,
+    // so it never completed. Mark them unsent so they are resent on reconnect.
+    // Android resends such gets too in production, though only because
+    // PersistentConnectionImpl.sendGet's early return sits inside
+    // `if (logger.logsDebug())`.
+    for (FOutstandingGet *get in [self.outstandingGets allValues]) {
+        get.sent = NO;
+    }
     self.unackedListensCount = 0;
     if ([self shouldReconnect]) {
         NSTimeInterval timeSinceLastConnectSucceeded =

Reproducing the issue

  1. Call getDataWithCompletionBlock: on a location without an active listener.
  2. Drop the connection after the request is written but before the response arrives. A proxy that holds the response makes this deterministic.
  3. Restore the connection. The completion block is never called.

Firebase SDK Version

12.8.0 (same code on main)

Firebase Product(s)

Database

Activity

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions