Skip to content

[firebase_database][Windows] Native RTDB error codes are lost when Pigeon/EventChannel errors omit details #18550

Description

@NH-Mason

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:

  1. In ParseError(), construct the three-argument FlutterError with a details map containing at
    least code and message.
  2. In both listener OnCancelled implementations, use the three-argument EventSink::Error with the
    same details map.
  3. Alternatively or defensively, change _flutterfire_internals to fall back to
    platformException.code when a details map does not contain a Firebase code.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions