Skip to content

fix(remoteconfig): buffer real-time stream data across network reads - #16828

Open
paulb777 wants to merge 2 commits into
mainfrom
pb-rc-realtime-stream-buffering
Open

paulb777 wants to merge 2 commits into
mainfrom
pb-rc-realtime-stream-buffering

Conversation

@paulb777

@paulb777 paulb777 commented Oct 2, 2026

Copy link
Copy Markdown
Member

Problem

-[RCNConfigRealtime URLSession:dataTask:didReceiveData:] parsed each network read on its own. It
looked for the first { and the next } within that read, parsed only that substring, and ignored
the rest.

The real-time stream is a JSON array of small config update objects ([{"latestTemplateVersionNumber": "5"}, then ,{...}), and NSURLSession can deliver it in arbitrary pieces. That caused three
problems:

A dropped latestTemplateVersionNumber message means no auto-fetch and no listener callback until a
later message or the next stream connection. When the #14094 crash was fixed in #14518, buffering
was named as the long-term fix. The Android and Web SDKs already accumulate stream data until a
message is complete.

Change

  • Each read is appended to a per-stream buffer (_pendingStreamData), and every complete message in
    it is handled (nextConfigUpdateMessage).
    • Data before a message's { (array punctuation, whitespace) is dropped.
    • A partial message is kept until its } arrives.
    • Matching works on bytes, so a UTF-8 sequence split across reads is reassembled too.
  • The buffer is cleared when a new stream response starts (didReceiveResponse:).
  • Once the server disables real-time updates, the stream is paused, so the rest of the buffered
    data is ignored. This case couldn't come up before, because at most one message per read was
    handled.
  • The 403 handling and evaluateStreamResponse:error: are unchanged. Messages are still assumed to
    be flat JSON objects, as before and as on Android/Web.
  • Added 5 tests to RCNRemoteConfigTest.m.
  • CHANGELOG entry.

Interplay with #16760, which also touches didReceiveData::

  • fix(remoteconfig): harden parsing of malformed fetch, realtime, and persisted data #16760 adds an early return for reads that aren't valid UTF-8. On main, those reads call
    +JSONObjectWithData: with nil, which raises.
  • With this change that path no longer crashes even without the early return: [nil containsString:] is NO, and the buffered bytes are parsed instead of the string.
  • Keeping the early return is harmless for the ASCII protocol messages. But it would drop bytes if a
    read ever ended mid-character, so it can go when the two are combined.
  • Its source hunks apply cleanly on this branch. Both PRs insert tests after
    testURLSessionDelegateHandlesChunkedJSON, so that's a trivial "keep both" conflict, plus the
    usual CHANGELOG one.

Testing

RCNRemoteConfigTest can't run on its own, on main or here. Its setUp creates an
RCNConfigDBManager directly, but gIsNewDatabaseQueue is only created in +sharedInstance. So
dispatch_sync on a NULL queue segfaults unless an earlier test class, such as
FIRRemoteConfigComponentTest, has already created the shared instance. Run the whole scheme, or
add -only-testing:RemoteConfigUnit/FIRRemoteConfigComponentTest.

xcodebuild -scheme RemoteConfigUnit test -destination 'platform=macOS'
  • Without the fix: 134 tests, 3 failures:
    • testRealtimeStreamHandlesMessageSplitAcrossReads evaluated nothing; expected [5].
    • testRealtimeStreamHandlesSeveralMessagesInOneRead evaluated [5]; expected [5, 6].
    • testRealtimeStreamHandlesReadsThatEndAndStartMessages evaluated [6]; expected [5, 6, 7].
  • With the fix: 134 tests, 0 failures.
  • Guard tests:
    • testRealtimeStreamDropsPartialMessageWhenNewResponseStarts and
      testRealtimeStreamStopsHandlingMessagesWhenRealtimeIsDisabled cover the buffer reset and the
      disabled check. They pass without the fix, because only one message per read was handled.
    • With the reset and the check temporarily removed from the fix, both fail: a parse error
      (<null>) instead of [6], and evaluateStreamResponse:error: called twice instead of once.

Fixes #16793

`URLSession:dataTask:didReceiveData:` parsed each network read on its own.
It looked for the first `{` and the next `}` within that read and ignored
everything else. The real-time stream is a JSON array of small config
update messages, and NSURLSession can deliver it in arbitrary pieces. So a
message split across reads was dropped, only the first of several messages
in one read was handled, and a read that finished one message and started
the next handled only the next one. A dropped template version message
meant no auto-fetch and no listener callback until a later message or the
next stream connection.

Buffer the stream data and handle every complete message, keeping a partial
message until the rest of it arrives, like the Android and Web SDKs do.
Clear the buffer when a new stream response starts, and stop handling
messages once the server disables real-time updates.

Fixes #16793
@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

@paulb777

paulb777 commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request improves Firebase Remote Config real-time updates by buffering incoming stream data in _pendingStreamData to ensure messages are not missed when split across network reads or received together in a single read. It introduces the nextConfigUpdateMessage helper to extract complete JSON objects from the buffer and includes comprehensive unit tests to verify these scenarios. Feedback suggests optimizing nextConfigUpdateMessage by using static NSData instances for the opening and closing braces to avoid redundant allocations.

Comment on lines +578 to +579
NSData *openingBrace = [NSData dataWithBytes:"{" length:1];
NSData *closingBrace = [NSData dataWithBytes:"}" length:1];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

To avoid allocating and deallocating NSData objects for the opening and closing braces on every call to nextConfigUpdateMessage, you can use static variables initialized once with dispatch_once.

  static NSData *openingBrace;
  static NSData *closingBrace;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    openingBrace = [NSData dataWithBytes:"{" length:1];
    closingBrace = [NSData dataWithBytes:"}" length:1];
  });

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[paulb777-bot] Thanks. nextConfigUpdateMessage runs once per received message, plus once to find that no complete message is left, and messages on this stream are rare, since they announce template updates. Two 1-byte NSData objects per message aren't measurable next to the JSON parsing and the fetch that each message triggers, so I've kept the code without static state.

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remote Config real-time stream can drop updates at callback boundaries

2 participants