-
Notifications
You must be signed in to change notification settings - Fork 30.1k
Description
What package does this bug report belong to?
go_router
What target platforms are you seeing this bug on?
Web
Have you already upgraded your packages?
Yes
Dependency versions
pubspec.lock
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896"
url: "https://pub.dev"
source: hosted
version: "17.1.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
version: "0.7.9"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
sdks:
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.35.0"
Steps to reproduce
Reproduction repository: https://github.com/davidmigloz/flutter_block_then_bug
Live demo: https://davidmigloz.github.io/flutter_block_then_bug/
Steps to reproduce
-
Clone the reproduction repository:
git clone https://github.com/davidmigloz/flutter_block_then_bug.git cd flutter_block_then_bug flutter pub get flutter run -d chrome -
The app starts authenticated on
/home. -
Tap "Sign Out (set false)" to toggle
isAuthenticatedtofalse. -
This triggers
refreshListenable, which re-evaluates theonEnterguard. The guard returns:Block.then(() => router.go('/login'));
-
Expected: The app navigates to
/login(green "FIXED" indicator). -
Actual: The app stays on
/home(red "BUG" indicator). The debug console confirms the callback fires ([Block.then] Calling router.go(/login)), but the navigation is silently lost.
Minimal code
final isAuthenticated = ValueNotifier<bool>(true);
GoRouter(
initialLocation: '/home',
refreshListenable: isAuthenticated,
onEnter: (context, current, next, router) {
if (next.matchedLocation == '/login') return const Allow();
if (!isAuthenticated.value) {
return Block.then(() => router.go('/login'));
}
return const Allow();
},
routes: [
GoRoute(path: '/home', builder: (_, _) => HomeScreen()),
GoRoute(path: '/login', builder: (_, _) => LoginScreen()),
],
);
// Toggling this triggers the bug:
isAuthenticated.value = false;Key observation
The same Block.then(() => router.go('/login')) works correctly when triggered by imperative navigation (router.go('/protected')), but fails when triggered by refreshListenable. The difference is that refreshListenable triggers route re-evaluation via notifyListeners() -> _processRouteInformation, and the callback's router.go() causes a re-entrant parse whose result is discarded by Flutter's Router transaction token mechanism.
Expected results
When refreshListenable fires and the onEnter guard returns Block.then(() => router.go('/login')), the app should navigate to /login.
The then callback documentation states it is "executed after the decision is committed" — so router.go() inside it should start a fresh navigation that commits normally, regardless of whether the guard was triggered by imperative navigation or by refreshListenable.
Actual results
The app stays on /home. The Block.then callback fires (confirmed by debug logs: [Block.then] Calling router.go(/login)), but the navigation to /login is silently lost. No error or warning is raised.
Root cause
In parser.dart, handleTopOnEnter executes the then callback synchronously via await Future<void>.sync(callback) while the current parse's Future chain is still in-flight. When the callback calls router.go('/login'):
GoRouteInformationProvider._setValuecallsnotifyListeners()synchronously- Flutter's
Routerreceives the notification and starts a new_processRouteInformation, minting a new transaction token (TokenB), replacing the outer parse's token (TokenA) - The re-entrant
/loginparse (FutureB) is scheduled but hasn't resolved yet Future.sync(callback)returns, and the outerhandleTopOnEnterreturns its "stay on current route"matchList- The outer parse (FutureA) resolves, but
_processParsedRouteInformationfinds TokenA is stale and silently discards the result - The
/loginparse result is also lost due to the interleaved transaction lifecycle
The net result: router.go('/login') fires without error, but the navigation is silently dropped by Flutter's Router transaction token mechanism.
Why it only fails with refreshListenable
The same Block.then(() => router.go('/login')) works when triggered by imperative navigation (e.g., router.go('/protected')). The difference is the call stack depth and transaction token lifecycle when refreshListenable triggers re-evaluation via its own notifyListeners() -> _processRouteInformation path.
Code sample
Code sample
[Paste your code here]Screenshots or Videos
Full reproduction repository: https://github.com/davidmigloz/flutter_block_then_bug
Code sample
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// Simulates an auth state that can be toggled.
final ValueNotifier<bool> isAuthenticated = ValueNotifier<bool>(true);
void main() => runApp(const App());
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
late final GoRouter _router;
@override
void initState() {
super.initState();
_router = GoRouter(
initialLocation: '/home',
refreshListenable: isAuthenticated,
onEnter: (context, current, next, router) {
final goingTo = next.matchedLocation;
debugPrint('[onEnter] authenticated=${isAuthenticated.value}, going to $goingTo');
// Public routes — always allow
if (goingTo == '/login') return const Allow();
// Protected routes — require auth
if (!isAuthenticated.value) {
debugPrint('[onEnter] Blocking, returning Block.then(go /login)');
return Block.then(() {
debugPrint('[Block.then] Calling router.go(/login)');
router.go('/login');
});
}
return const Allow();
},
routes: [
GoRoute(path: '/home', builder: (_, _) => const Scaffold(body: Center(child: Text('Home')))),
GoRoute(path: '/login', builder: (_, _) => const Scaffold(body: Center(child: Text('Login')))),
],
);
}
@override
void dispose() {
_router.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(routerConfig: _router);
}
}
// To trigger the bug:
// 1. App starts authenticated on /home
// 2. Toggle: isAuthenticated.value = false;
// 3. refreshListenable fires, guard returns Block.then(() => router.go('/login'))
// 4. Expected: navigates to /login
// 5. Actual: stays on /home — callback navigation silently lostLogs
Logs
// Debug console output after tapping "Sign Out" (toggling isAuthenticated to false):
[onEnter] authenticated=false, going to /home
[onEnter] Blocking, returning Block.then(go /login)
[Block.then] Calling router.go(/login)
[onEnter] authenticated=false, going to /login
// The guard correctly evaluates, the Block.then callback fires, router.go('/login') is called,
// and onEnter even runs for /login and returns Allow — but the app stays on /home.
// No error or warning is logged. The navigation is silently lost.Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.41.2, on macOS 26.3 25D125 darwin-arm64, locale en-US)
• Flutter version 3.41.2 on channel stable
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 90673a4eef (9 days ago), 2026-02-18 13:54:59 -0800
• Engine revision 6c0baaebf7
• Dart version 3.11.0
• DevTools version 2.54.1
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
• Platform android-36, build-tools 36.0.0
• Java version OpenJDK Runtime Environment (build 21.0.6+-13391695-b895.109)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 26.3)
• Build 17C529
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (2 available)
• macOS (desktop) • macos • darwin-arm64 • macOS 26.3 25D125 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 145.0.7632.117
[✓] Network resources
• All expected network resources are available.
• No issues found!