Summary
firebase_database 12.4.6 for Windows computes useful Realtime Database error strings such as
permission-denied and write-canceled, but its native host-API error helper constructs the
two-argument FlutterError(code, message). That leaves Pigeon's details value null.
_flutterfire_internals 1.3.75 then reads the Firebase code only from PlatformException.details,
not from PlatformException.code, and therefore returns FirebaseException(code: 'unknown').
The value- and child-listener cancellation paths have the same defect independently: both call the
two-argument EventSink::Error(code, message), whose Flutter C++ client wrapper explicitly passes a
null details pointer.
This report is about native Firebase C++ errors emitted through those Windows host-API and listener
paths. It does not claim that Dart-side validation/channel errors follow the same route.
One important qualification: a useful mapped code is consistently lost on these null-details paths,
but the message is not necessarily empty. _flutterfire_internals initially preserves
PlatformException.message; an empty final message occurs when the native SDK/plugin supplied an
empty message, as in the runtime-observed write-canceled case related below.
Environment
| Package/component |
Version / source pin |
firebase_database |
12.4.6; tag firebase_database-v12.4.6 at commit 011bd5d8b0be072144bf949b715664a284698c99 |
firebase_database_platform_interface |
0.4.0+5; tag firebase_database_platform_interface-v0.4.0+5 at the same commit |
_flutterfire_internals |
1.3.75; tag _flutterfire_internals-v1.3.75 at the same commit |
firebase_core |
4.12.1 |
Firebase C++ SDK selected by firebase_core on Windows |
13.9.0 |
| Flutter |
3.47.0-0.1.pre beta |
| Platform |
Windows 11 x64 |
The cited tagged files were rechecked against the corresponding pub-cache packages; the relevant
contents and line numbers match. firebase_core 4.12.1 pins the Windows C++ SDK at 13.9.0 in
windows/CMakeLists.txt:7.
Runtime-observed case
The previously reported Windows reproduction in
flutterfire#18549 deliberately aborts an RTDB
transaction:
try {
await FirebaseDatabase.instance.ref('some/path').runTransaction((current) {
return Transaction.abort();
});
} on FirebaseException catch (error) {
print(error.code); // unknown
print(error.message); // ''
}
That run observed FirebaseException(code: 'unknown', message: ''). The native error 11/no-message
result and the intermediate PlatformException(code: 'write-canceled', details: null) were reported
in #18549 and independently match the pinned source chain below. The separate abort-contract defect
is why that transaction threw at all; this report is the distinct conversion defect that changed its
useful native code to unknown.
Isolated reproduction shape
Any Windows host-API operation that completes with a mapped native error exercises the same chain.
For example, against a path whose Realtime Database rules deny writes:
try {
await FirebaseDatabase.instance.ref('denied/path').set('probe');
} on FirebaseException catch (error) {
print('${error.code}: ${error.message}');
}
Expected: error.code == 'permission-denied' and the native message is preserved.
Source-predicted actual on 12.4.6/1.3.75: error.code == 'unknown'; the message is whatever the native
future supplied.
[NEEDS VERIFICATION: standalone denied-write runtime capture] The retained runtime capture uses
the write-canceled transaction path above. The denied-write example isolates the diagnostics bug
more cleanly, but has not yet been captured as a separate run.
For listeners, attach a value or child listener to a path that the server cancels with a native error.
The listener path is independently confirmed from source; a standalone listener-cancellation runtime
capture is also [NEEDS VERIFICATION].
Expected result
- A mapped Windows native code survives as
FirebaseException.code.
- A non-empty native message survives as
FirebaseException.message.
- Host-API failures and listener cancellations follow the same Firebase exception contract.
Actual result
Root cause with pinned citations
1. The Windows plugin computes a useful code, then omits details
GetDatabaseErrorCode() maps the C++ enum to strings including permission-denied and
write-canceled. ParseError() passes that code and the native future message to a two-argument
FlutterError:
firebase_database_plugin.cpp:194-241
std::string code = GetDatabaseErrorCode(error);
std::string message =
future.error_message() ? future.error_message() : "Unknown error";
return FlutterError(code, message);
The generated class has distinct two- and three-argument constructors. Only the latter initializes
details_ with a supplied value:
messages.g.h:22-38
2. Generated Pigeon code serializes that null details value
The Windows reply wrapper serializes [code, message, details] and receives the default/null
details_ from the two-argument constructor:
messages.g.cpp:2058-2068
The Dart side creates a PlatformException using those three positions:
messages.pigeon.dart:15-30
Thus the useful string is still present as PlatformException.code, while
PlatformException.details == null.
3. _flutterfire_internals ignores PlatformException.code
Reference operations catch the Pigeon exception and pass it to convertPlatformException; this is
the path used by both the denied-write example and the transaction case above:
The converter initializes code to null and assigns it only from details['code']; it ultimately
uses code ?? 'unknown'. It does preserve platformException.message unless details overrides it:
exception.dart:37-63
4. Listener cancellations omit details separately
Both listener implementations call events_->Error(code, message) without a third argument:
firebase_database_plugin.cpp:965-1020
At the pinned Flutter engine revision, that two-argument overload calls ErrorInternal(..., nullptr):
event_sink.h:31-40
The Dart query stream passes EventChannel failures to convertPlatformException, and that adapter
delegates to the same _flutterfire_internals converter described above:
Suggested fix
Either repair both native emission paths or make the common Dart converter tolerate native plugins
that put the code in the standard PlatformException.code field:
- In
ParseError(), construct the three-argument FlutterError with a details map containing at
least code and message.
- In both listener
OnCancelled implementations, use the three-argument EventSink::Error with the
same details map.
- Alternatively or defensively, change
_flutterfire_internals to fall back to
platformException.code when a details map does not contain a Firebase code.
- Add Windows tests for both a Pigeon host-API error and an EventChannel listener error, asserting
that a mapped code and a non-empty message survive conversion.
The Dart fallback is the smallest common fix and protects other native plugins with the same shape;
populating details in firebase_database also makes its platform payload conform to what the current
converter expects.
Workaround
There is no reliable general application-layer workaround after the public API has emitted
FirebaseException(code: 'unknown'), especially when the native message is also empty. A temporary
fork can either populate details in both Windows plugin paths or apply the
PlatformException.code fallback in _flutterfire_internals. Mapping unknown based only on which
operation was attempted is ambiguous and should not be treated as a durable fix.
Related but distinct
flutterfire#18549 reports that an initial
Windows Transaction.abort() throws instead of resolving with committed: false. The defect here is
why that issue's thrown exception is displayed as unknown with an empty message. Fixing this report
restores diagnostics; it does not by itself repair the transaction-abort contract.
Remaining verification gaps
- [NEEDS VERIFICATION] Standalone Windows denied-write runtime capture.
- [NEEDS VERIFICATION] Standalone Windows listener-cancellation runtime capture.
Summary
firebase_database12.4.6 for Windows computes useful Realtime Database error strings such aspermission-deniedandwrite-canceled, but its native host-API error helper constructs thetwo-argument
FlutterError(code, message). That leaves Pigeon'sdetailsvalue null._flutterfire_internals1.3.75 then reads the Firebase code only fromPlatformException.details,not from
PlatformException.code, and therefore returnsFirebaseException(code: 'unknown').The value- and child-listener cancellation paths have the same defect independently: both call the
two-argument
EventSink::Error(code, message), whose Flutter C++ client wrapper explicitly passes anull details pointer.
This report is about native Firebase C++ errors emitted through those Windows host-API and listener
paths. It does not claim that Dart-side validation/channel errors follow the same route.
One important qualification: a useful mapped code is consistently lost on these null-details paths,
but the message is not necessarily empty.
_flutterfire_internalsinitially preservesPlatformException.message; an empty final message occurs when the native SDK/plugin supplied anempty message, as in the runtime-observed
write-canceledcase related below.Environment
firebase_database12.4.6; tagfirebase_database-v12.4.6at commit011bd5d8b0be072144bf949b715664a284698c99firebase_database_platform_interface0.4.0+5; tagfirebase_database_platform_interface-v0.4.0+5at the same commit_flutterfire_internals1.3.75; tag_flutterfire_internals-v1.3.75at the same commitfirebase_core4.12.1firebase_coreon Windows13.9.03.47.0-0.1.prebetaThe cited tagged files were rechecked against the corresponding pub-cache packages; the relevant
contents and line numbers match.
firebase_core4.12.1 pins the Windows C++ SDK at 13.9.0 inwindows/CMakeLists.txt:7.Runtime-observed case
The previously reported Windows reproduction in
flutterfire#18549 deliberately aborts an RTDB
transaction:
That run observed
FirebaseException(code: 'unknown', message: ''). The native error 11/no-messageresult and the intermediate
PlatformException(code: 'write-canceled', details: null)were reportedin #18549 and independently match the pinned source chain below. The separate abort-contract defect
is why that transaction threw at all; this report is the distinct conversion defect that changed its
useful native code to
unknown.Isolated reproduction shape
Any Windows host-API operation that completes with a mapped native error exercises the same chain.
For example, against a path whose Realtime Database rules deny writes:
Expected:
error.code == 'permission-denied'and the native message is preserved.Source-predicted actual on 12.4.6/1.3.75:
error.code == 'unknown'; the message is whatever the nativefuture supplied.
[NEEDS VERIFICATION: standalone denied-write runtime capture] The retained runtime capture uses
the
write-canceledtransaction path above. The denied-write example isolates the diagnostics bugmore cleanly, but has not yet been captured as a separate run.
For listeners, attach a value or child listener to a path that the server cancels with a native error.
The listener path is independently confirmed from source; a standalone listener-cancellation runtime
capture is also [NEEDS VERIFICATION].
Expected result
FirebaseException.code.FirebaseException.message.Actual result
PlatformException.codeand null
details; the converter ignores that code and emitsunknown.is lost by the same converter.
unknown/empty symptom in [firebase_database] Windows: a deliberateTransaction.abort()throwsFirebaseExceptioninstead of returningTransactionResult(committed: false)#18549,but empty messages are not universal across all native RTDB failures.
Root cause with pinned citations
1. The Windows plugin computes a useful code, then omits details
GetDatabaseErrorCode()maps the C++ enum to strings includingpermission-deniedandwrite-canceled.ParseError()passes that code and the native future message to a two-argumentFlutterError:firebase_database_plugin.cpp:194-241std::string code = GetDatabaseErrorCode(error); std::string message = future.error_message() ? future.error_message() : "Unknown error"; return FlutterError(code, message);The generated class has distinct two- and three-argument constructors. Only the latter initializes
details_with a supplied value:messages.g.h:22-382. Generated Pigeon code serializes that null details value
The Windows reply wrapper serializes
[code, message, details]and receives the default/nulldetails_from the two-argument constructor:messages.g.cpp:2058-2068The Dart side creates a
PlatformExceptionusing those three positions:messages.pigeon.dart:15-30Thus the useful string is still present as
PlatformException.code, whilePlatformException.details == null.3.
_flutterfire_internalsignoresPlatformException.codeReference operations catch the Pigeon exception and pass it to
convertPlatformException; this isthe path used by both the denied-write example and the transaction case above:
method_channel_database_reference.dart:85-97method_channel_database_reference.dart:150-191The converter initializes
codeto null and assigns it only fromdetails['code']; it ultimatelyuses
code ?? 'unknown'. It does preserveplatformException.messageunless details overrides it:exception.dart:37-634. Listener cancellations omit details separately
Both listener implementations call
events_->Error(code, message)without a third argument:firebase_database_plugin.cpp:965-1020At the pinned Flutter engine revision, that two-argument overload calls
ErrorInternal(..., nullptr):event_sink.h:31-40The Dart query stream passes EventChannel failures to
convertPlatformException, and that adapterdelegates to the same
_flutterfire_internalsconverter described above:method_channel_query.dart:49-68utils/exception.dart:10-20_flutterfire_internals/exception.dart:66-81Suggested fix
Either repair both native emission paths or make the common Dart converter tolerate native plugins
that put the code in the standard
PlatformException.codefield:ParseError(), construct the three-argumentFlutterErrorwith a details map containing atleast
codeandmessage.OnCancelledimplementations, use the three-argumentEventSink::Errorwith thesame details map.
_flutterfire_internalsto fall back toplatformException.codewhen a details map does not contain a Firebase code.that a mapped code and a non-empty message survive conversion.
The Dart fallback is the smallest common fix and protects other native plugins with the same shape;
populating details in
firebase_databasealso makes its platform payload conform to what the currentconverter expects.
Workaround
There is no reliable general application-layer workaround after the public API has emitted
FirebaseException(code: 'unknown'), especially when the native message is also empty. A temporaryfork can either populate details in both Windows plugin paths or apply the
PlatformException.codefallback in_flutterfire_internals. Mappingunknownbased only on whichoperation was attempted is ambiguous and should not be treated as a durable fix.
Related but distinct
flutterfire#18549 reports that an initial
Windows
Transaction.abort()throws instead of resolving withcommitted: false. The defect here iswhy that issue's thrown exception is displayed as
unknownwith an empty message. Fixing this reportrestores diagnostics; it does not by itself repair the transaction-abort contract.
Remaining verification gaps