-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[firebase_database]: Listener throws permission denied
after sign out despite awaiting
the StreamSubscription
to be canceled
before
#16726
Labels
Needs Attention
This issue needs maintainer attention.
platform: android
Issues / PRs which are specifically for Android.
platform: ios
Issues / PRs which are specifically for iOS.
plugin: database
type: bug
Something isn't working
Comments
Tom3652
added
Needs Attention
This issue needs maintainer attention.
type: bug
Something isn't working
labels
Nov 19, 2024
SelaseKay
added
plugin: database
platform: android
Issues / PRs which are specifically for Android.
platform: ios
Issues / PRs which are specifically for iOS.
labels
Nov 20, 2024
Hi @Tom3652, thanks for the report. I'm able to reproduce this issue. Reproducible codeReproducible code// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_database/ui/firebase_animated_list.dart';
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb;
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_options.dart';
// Change to false to use live database instance.
const USE_DATABASE_EMULATOR = false;
// The port we've set the Firebase Database emulator to run on via the
// `firebase.json` configuration file.
const emulatorPort = 9000;
// Android device emulators consider localhost of the host machine as 10.0.2.2
// so let's use that if running on Android.
final emulatorHost =
(!kIsWeb && defaultTargetPlatform == TargetPlatform.android)
? '10.0.2.2'
: 'localhost';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
if (USE_DATABASE_EMULATOR) {
FirebaseDatabase.instance.useDatabaseEmulator(emulatorHost, emulatorPort);
}
runApp(
const MaterialApp(
title: 'Flutter Database Example',
home: TestApp(),
),
);
}
class TestApp extends StatefulWidget {
const TestApp({super.key});
@override
State<TestApp> createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
StreamSubscription? _listener;
Future<void> register() async {
_listener =
FirebaseDatabase.instance.ref().child("test_path").onValue.listen((d) {
print("Listner value: ${d.snapshot.value}");
});
}
Future<void> signIn() async {
print("Sign in");
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: "your-email", password: "your-password");
register();
}
Future<void> signOut() async {
print("Sign out !");
await _listener?.cancel();
await FirebaseAuth.instance.signOut();
}
@override
void initState() {
signIn();
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(onPressed: () {
signIn();
}, child: Text("Sign in")),
SizedBox(
height: 30,
),
ElevatedButton(onPressed: () {
signOut();
}, child: Text("Sign out"))
],
),
),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
late DatabaseReference _counterRef;
late DatabaseReference _messagesRef;
late StreamSubscription<DatabaseEvent> _counterSubscription;
late StreamSubscription<DatabaseEvent> _messagesSubscription;
bool _anchorToBottom = false;
String _kTestKey = 'Hello';
String _kTestValue = 'world!';
FirebaseException? _error;
bool initialized = false;
@override
void initState() {
init();
super.initState();
}
Future<void> init() async {
_counterRef = FirebaseDatabase.instance.ref('counter');
final database = FirebaseDatabase.instance;
_messagesRef = database.ref('messages');
database.setLoggingEnabled(false);
if (!kIsWeb) {
database.setPersistenceEnabled(true);
database.setPersistenceCacheSizeBytes(10000000);
}
if (!kIsWeb) {
await _counterRef.keepSynced(true);
}
setState(() {
initialized = true;
});
try {
final counterSnapshot = await _counterRef.get();
print(
'Connected to directly configured database and read '
'${counterSnapshot.value}',
);
} catch (err) {
print(err);
}
_counterSubscription = _counterRef.onValue.listen(
(DatabaseEvent event) {
setState(() {
_error = null;
_counter = (event.snapshot.value ?? 0) as int;
});
},
onError: (Object o) {
final error = o as FirebaseException;
setState(() {
_error = error;
});
},
);
final messagesQuery = _messagesRef.limitToLast(10);
_messagesSubscription = messagesQuery.onChildAdded.listen(
(DatabaseEvent event) {
print('Child added: ${event.snapshot.value}');
},
onError: (Object o) {
final error = o as FirebaseException;
print('Error: ${error.code} ${error.message}');
},
);
}
@override
void dispose() {
super.dispose();
_messagesSubscription.cancel();
_counterSubscription.cancel();
}
Future<void> _increment() async {
await _counterRef.set(ServerValue.increment(1));
await _messagesRef
.push()
.set(<String, String>{_kTestKey: '$_kTestValue $_counter'});
}
Future<void> _incrementAsTransaction() async {
try {
final transactionResult = await _counterRef.runTransaction((mutableData) {
return Transaction.success((mutableData as int? ?? 0) + 1);
});
if (transactionResult.committed) {
final newMessageRef = _messagesRef.push();
await newMessageRef.set(<String, String>{
_kTestKey: '$_kTestValue ${transactionResult.snapshot.value}',
});
}
} on FirebaseException catch (e) {
print(e.message);
}
}
Future<void> _deleteMessage(DataSnapshot snapshot) async {
final messageRef = _messagesRef.child(snapshot.key!);
await messageRef.remove();
}
void _setAnchorToBottom(bool? value) {
if (value == null) {
return;
}
setState(() {
_anchorToBottom = value;
});
}
@override
Widget build(BuildContext context) {
if (!initialized) return Container();
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Database Example'),
),
body: Column(
children: [
Flexible(
child: Center(
child: _error == null
? Text(
'Button tapped $_counter time${_counter == 1 ? '' : 's'}.\n\n'
'This includes all devices, ever.',
)
: Text(
'Error retrieving button tap count:\n${_error!.message}',
),
),
),
ElevatedButton(
onPressed: _incrementAsTransaction,
child: const Text('Increment as transaction'),
),
ListTile(
leading: Checkbox(
onChanged: _setAnchorToBottom,
value: _anchorToBottom,
),
title: const Text('Anchor to bottom'),
),
Flexible(
child: FirebaseAnimatedList(
key: ValueKey<bool>(_anchorToBottom),
query: _messagesRef,
reverse: _anchorToBottom,
itemBuilder: (context, snapshot, animation, index) {
return SizeTransition(
sizeFactor: animation,
child: ListTile(
trailing: IconButton(
onPressed: () => _deleteMessage(snapshot),
icon: const Icon(Icons.delete),
),
title: Text('$index: ${snapshot.value}'),
),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
|
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
Needs Attention
This issue needs maintainer attention.
platform: android
Issues / PRs which are specifically for Android.
platform: ios
Issues / PRs which are specifically for iOS.
plugin: database
type: bug
Something isn't working
Is there an existing issue for this?
Which plugins are affected?
Database
Which platforms are affected?
Android, iOS
Description
When you
cancel
theStreamSubscription
listener of a Firebase and sign out right after, there is a chance to get a race condition where FirebaseDatabase throws apermission denied
error when the listener should already be cancelled.Actual results : In my real app, i am cancelling all the listeners i have to my
FirebaseDatabase
instance, then i sign out the user when he clicks onSign out
.That works fine most of the time, but among my few thousands active users, i have few hundreds of "Permission denied" logs in Crashlytics.
Moreover, i have managed to reproduce it on my local machine, this happens often enough on both Android and iOS to be noticeable.
Expected results : When calling
await _listener?.cancel()
on aFirebaseDatabase.instance.ref().onValue.listen((d) {})
StreamSubscription
, i would like the stream to be canceled at 100% when theawait
completes, which would avoid all these scenarios with permission denied errors.Reproducing the issue
auth.uid != null
in the Firebase console on the testing nodeFirebase Core version
3.8.0
Flutter Version
3.24.5
Relevant Log Output
Flutter dependencies
Expand
Flutter dependencies
snippetAdditional context and comments
No response
The text was updated successfully, but these errors were encountered: