[go_router] Fix Block.then() re-entrancy when triggered by refreshListenable#11136
Open
davidmigloz wants to merge 1 commit intoflutter:mainfrom
Open
[go_router] Fix Block.then() re-entrancy when triggered by refreshListenable#11136davidmigloz wants to merge 1 commit intoflutter:mainfrom
davidmigloz wants to merge 1 commit intoflutter:mainfrom
Conversation
…tenable `Block.then(() => router.go(...))` callbacks silently lose their navigation when triggered by `refreshListenable`. The `router.go()` inside the callback runs synchronously during `handleTopOnEnter`, triggering a re-entrant `_processRouteInformation` whose result is dropped due to transaction token churn in Flutter's Router. Fix: wrap the `then` callback in `scheduleMicrotask()` so it runs after the current parse completes and Flutter's Router has committed the result. This is consistent with the `then` documentation which states the callback is "executed after the decision is committed". Adds 5 regression tests covering Block.then + refreshListenable, goNamed variant, rapid emissions, Allow.then, and error propagation. Fixes flutter/flutter#183012
There was a problem hiding this comment.
Code Review
This pull request addresses a re-entrancy issue in go_router where navigations within Block.then() and Allow.then() callbacks, triggered by refreshListenable, were being lost. The fix defers the execution of these callbacks using scheduleMicrotask, ensuring they run after the current route parsing is complete, thus preventing the re-entrancy problem. The change is accompanied by a comprehensive suite of new regression tests covering various scenarios, including router.go, router.goNamed, rapid refreshListenable events, and error propagation. A corresponding changelog entry has also been added.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes flutter/flutter#183012
Block.then(() => router.go(...))andAllow(then: () => router.go(...))callbacks inonEntersilently lose their navigation when triggered byrefreshListenable. Therouter.go()inside the callback runs synchronously duringhandleTopOnEnterprocessing, triggering a re-entrant_processRouteInformationwhose result is dropped due to transaction token churn in Flutter'sRouter.Reproduction repo: https://github.com/davidmigloz/flutter_block_then_bug
Live demo: https://davidmigloz.github.io/flutter_block_then_bug/
Root Cause
In
parser.dart,handleTopOnEnterexecutes thethencallback viaawait Future<void>.sync(callback)(line 533). When the callback callsrouter.go('/login'), it synchronously triggersnotifyListeners()→_processRouteInformationwhile the outer parse is still in-flight. Flutter'sRoutermints a new transaction token for the re-entrant parse and silently discards the result.Fix
Replace
await Future<void>.sync(callback)withscheduleMicrotask(() async { await callback(); }). This defers the callback to the microtask queue, ensuringrouter.go()runs after the current parse has completed and Flutter'sRouterhas committed the result.This is the minimal deferral (before any timer/frame), and the behavior change (callbacks fire asynchronously instead of synchronously) is consistent with the documentation ("Executed after the decision is committed").
Tests
on_enter_test.dart):Block.then(router.go)+refreshListenable,Block.then(router.goNamed)variant, rapidrefreshListenableemissions,Allow.then(router.go)variant, error propagation after deferral.Pre-Review Checklist
[shared_preferences]pubspec.yamlwith an appropriate new version according to the pub versioning philosophy, or I have commented below to indicate which version change exemption this PR falls under1.CHANGELOG.mdto add a description of the change, following repository CHANGELOG style, or I have commented below to indicate which CHANGELOG exemption this PR falls under1.///).Footnotes
This PR uses
pending_changelogs/for versioning and changelog, following the go_router batch release process. ↩ ↩2 ↩3