Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[go_router] Block.then(() => router.go(...)) navigation silently lost when triggered by refreshListenable #183012

@davidmigloz

Description

@davidmigloz

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

  1. 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
  2. The app starts authenticated on /home.

  3. Tap "Sign Out (set false)" to toggle isAuthenticated to false.

  4. This triggers refreshListenable, which re-evaluates the onEnter guard. The guard returns:

    Block.then(() => router.go('/login'));
  5. Expected: The app navigates to /login (green "FIXED" indicator).

  6. 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'):

  1. GoRouteInformationProvider._setValue calls notifyListeners() synchronously
  2. Flutter's Router receives the notification and starts a new _processRouteInformation, minting a new transaction token (TokenB), replacing the outer parse's token (TokenA)
  3. The re-entrant /login parse (FutureB) is scheduled but hasn't resolved yet
  4. Future.sync(callback) returns, and the outer handleTopOnEnter returns its "stay on current route" matchList
  5. The outer parse (FutureA) resolves, but _processParsedRouteInformation finds TokenA is stale and silently discards the result
  6. The /login parse 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 lost

Logs

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listhas reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: go_routerThe go_router packagepackageflutter/packages repository. See also p: labels.platform-webWeb applications specificallyteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions