Repository navigation
Conversation
When the server answered a get() request with data that NodeUtilities.NodeFromJSON can't parse, for example a node whose .priority is an object, an array, or a boolean, the DatabaseException escaped the Task listener in Repo.getValue. That listener runs on the run loop executor, and the Android run loop rethrows uncaught exceptions on the main thread, so the app crashed. The get() Task also never completed. The same data in a regular push is already caught and logged in Repo.onDataUpdate. Nothing was persisted. Catch the DatabaseException from the parse step, log it, and complete the Task the same way as a failed request: return the persisted value if there is one, otherwise fail the Task with the exception. Parsing still happens before the temporary keep-synced registration, so nothing is registered or written to the cache when it fails. Add RepoGetValueTest, which drives get() through a Repo with a mocked PersistentConnection and a run loop that records uncaught exceptions.
Using Gemini Code AssistThe 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
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 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. |
📝 PRs merging into main branchOur main branch should always be in a releasable state. If you are working on a larger change, or if you don't want this change to see the light of the day just yet, consider using a feature branch first, and only merge into the main branch when the code complete and ready to be released. |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request addresses a crash in the Firebase Database SDK when the server returns data that cannot be parsed during a get() call. The SDK now catches DatabaseException during parsing, failing the task or falling back to a locally cached value if one exists, and a new test suite has been added to verify these scenarios. Feedback was provided to format the CHANGELOG entry to comply with the repository's 78-character line limit rule.
| - [fixed] Fixed a crash when the server's response to a `get()` call contained | ||
| data that the SDK couldn't parse. The `Task` now fails instead, or returns | ||
| the locally cached value if there is one. (#8777) |
There was a problem hiding this comment.
The first line of the CHANGELOG entry exceeds the 78-character limit specified in the repository style guide. Keeping the first line to at most 78 characters ensures it remains within 80 characters when the SCM/release-notes generator converts - [fixed] to * {{fixed}}.
| - [fixed] Fixed a crash when the server's response to a `get()` call contained | |
| data that the SDK couldn't parse. The `Task` now fails instead, or returns | |
| the locally cached value if there is one. (#8777) | |
| - [fixed] Fixed a crash when the server's response to a `get()` call | |
| contained data that the SDK couldn't parse. The `Task` now fails instead, | |
| or returns the locally cached value if there is one. (#8777) |
References
- When writing CHANGELOG.md entries, keep the first line to at most 78 characters so that it remains within 80 characters when the tag is converted in generated release notes. (link)
There was a problem hiding this comment.
Done in 9e53878: rewrapped the entry so the first line is 68 characters (it was exactly 78, so at the limit rather than over it).
Rewrap the get() crash entry so that its first line is 68 characters, well under the 78-character limit for CHANGELOG first lines.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request fixes a crash in the Firebase Database SDK that occurs when the server's response to a get() call contains unparseable data, such as an invalid priority. The fix wraps the JSON parsing logic in a try-catch block within Repo.java. If a DatabaseException is caught, the SDK now logs the error and either returns a locally cached value if available or fails the task with the exception, rather than crashing the application. Additionally, a comprehensive set of unit tests has been added in RepoGetValueTest.java to verify this behavior under various scenarios, and the CHANGELOG.md has been updated accordingly. I have no feedback to provide as there are no review comments.
Summary
DatabaseReference.get()andQuery.get()no longer crash the app when theserver's response contains data that the SDK can't parse. The returned
Tasknow completes the same way as a failed request.
Problem
This comes from an expanded audit of how the SDK handles unexpected server data,
which followed
firebase/firebase-ios-sdk#16728.
Repo.getValueparses the server's response to aget()request withNodeUtilities.NodeFromJSONinside aTasklistener that runs on the databaserun loop.
NodeFromJSONthrowsDatabaseExceptionfor data it can't parse, forexample a node whose
.priorityis an object, an array, or a boolean (seePriorityUtilities.parsePriority). The exception wasn't caught, so it reachedDefaultRunLoop.afterExecute, and theAndroidPlatformrun loop rethrows it onthe main thread, which crashes the app. The
get()Taskalso never completed.The same data in a regular push is already handled:
Repo.onDataUpdatecatchesDatabaseExceptionand logsFIREBASE INTERNAL ERROR.The trigger needs a backend bug, regression, or protocol change. The bad value
isn't persisted, because the exception is thrown before the response is applied
to the cache. The crashes stop once the server is fixed, but until then every
get()that receives such data crashes the app.The connection-level safety net in #8764 doesn't cover this case: the
get()result reaches
Repothrough aTasklistener that runs as a separate run looptask, after
Connection.onMessagehas returned.Fix
In
Repo.getValue, the call toNodeUtilities.NodeFromJSONis now wrapped in atry/catch (DatabaseException e). On failure, the SDK logs an error throughoperationLogger(a short message and the stack trace, not the payload) andcompletes the
Taskthe same way as a failed request: it returns the persistedvalue if there is one, otherwise it fails the
Taskwith theDatabaseException. Parsing already happened before the temporary keep-syncedregistration (
keepSynced(spec, true, true)), and the new code returns beforeit, so nothing is registered, applied to the sync tree, or written to the cache.
Alternatives considered:
RuntimeExceptionaround the whole listener: rejected, because itwould also hide bugs in the sync tree code that runs after parsing.
DatabaseExceptionis whatNodeFromJSONthrows for data it can't parse (italso wraps
ClassCastException).parsed: rejected, because
get()would then report data that doesn't matchthe server without any error.
AndroidPlatform: rejected, becauseit would also hide unrelated bugs.
Behavior change for reviewers: a
get()that used to crash now fails with aDatabaseException(for example "Node contains invalid priority: Must be astring, double, ServerValue, or null"), or returns the persisted value when the
persistence cache has one. This matches what
get()already does when therequest itself fails.
Testing
New Robolectric test class
RepoGetValueTestinfirebase-database/src/test/java/com/google/firebase/database/core/. It builds aRepofrom aDatabaseConfigwhosenewPersistentConnectionreturns a Mockitomock of
PersistentConnection, so the tests control the response toget()without a network connection or production test hooks. The run loop records
uncaught exceptions instead of crashing, and every test asserts that none
reached it. Responses are parsed with
JsonMapper.parseJsonValue, the same wayas messages from the server.
getFailsWhenPriorityIsAnObject:{"a": 1, ".priority": {"x": 1}}getFailsWhenPriorityIsABoolean:{".priority": true, ".value": 1}getFailsWhenListElementHasInvalidPriority:["a", {".priority": ["x"], ".value": "b"}]queryGetFailsWhenChildHasInvalidPriority:Query.get()withorderByKey().limitToFirst(1). It also checks that no keep-syncedregistration is left behind.
getReturnsPersistedValueWhenResponseCannotBeParsed: with a value in thepersistence cache,
get()returns that value.getReturnsServerValueandqueryGetReturnsServerValue: valid responses,including a valid priority, still return the right snapshot.
Before the fix, 5 of the 7 tests failed. The four tests without a persisted
value timed out because the
Tasknever completed. The persisted-value testfound the
DatabaseExceptionon the run loop; itsTaskonly completed throughthe existing 3-second persisted-value fallback. The two valid-response tests
passed before and after the fix.
Commands:
./gradlew --max-workers=4 :firebase-database:spotlessApply ./gradlew --max-workers=4 :firebase-database:testDebugUnitTest --tests "com.google.firebase.database.core.RepoGetValueTest" ./gradlew --max-workers=4 :firebase-database:testDebugUnitTestResults:
spotlessApplysucceeded.RepoGetValueTestran 7 tests with 0failures (before the fix: 7 tests, 5 failures). The full module run ran 410
tests with 0 failures and 2 skipped (both already marked
@Ignore).