Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5cea2f7
feat: access GoRouter.of from redirect methods
tomassasovsky Jul 30, 2025
05b94b7
refactor: update documentation for navigatorKey and extraCodec in Rou…
tomassasovsky Jul 30, 2025
c93570b
refactor: replace _currentRouterKey with currentRouterKey constant fo…
tomassasovsky Jul 30, 2025
c94be6f
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Jul 30, 2025
858ea74
chore: release version 16.0.1 with fixes for redirect callbacks and c…
tomassasovsky Jul 31, 2025
d2b6d00
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Jul 31, 2025
d90201c
Merge branch 'feat/go-router-context-redirect' of github.com:tomassas…
tomassasovsky Jul 31, 2025
61bc8ff
refactor: update documentation for GoRouter context methods to clarif…
tomassasovsky Jul 31, 2025
6309387
feat: implement error handling in router zone for GoRouter
tomassasovsky Aug 1, 2025
fec0356
refactor: replace runZoned with runZonedGuarded for improved error ha…
tomassasovsky Aug 2, 2025
e756161
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Aug 2, 2025
2cdaa91
feat: enhance error handling in redirect callbacks and tests for GoRo…
tomassasovsky Aug 5, 2025
52d78a7
Merge branch 'feat/go-router-context-redirect' of github.com:tomassas…
tomassasovsky Aug 5, 2025
6221028
Merge branch 'main' of github.com:flutter/packages into feat/go-route…
tomassasovsky Sep 24, 2025
05c1cbc
chore: dart format
tomassasovsky Sep 25, 2025
7d8d0d5
chore: add copyright to constants.dart
tomassasovsky Sep 25, 2025
7beaf8e
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Sep 25, 2025
1619f40
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Sep 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions packages/go_router/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import 'route.dart';
import 'router.dart';
import 'state.dart';

/// Symbol used as a Zone key to track the current GoRouter during redirects.
const Symbol _currentRouterKey = #goRouterRedirectContext;

/// The signature of the redirect callback.
typedef GoRouterRedirect = FutureOr<String?> Function(
BuildContext context, GoRouterState state);
Expand All @@ -29,6 +32,7 @@ class RouteConfiguration {
this._routingConfig, {
required this.navigatorKey,
this.extraCodec,
this.router,
}) {
_onRoutingTableChanged();
_routingConfig.addListener(_onRoutingTableChanged);
Expand Down Expand Up @@ -248,6 +252,10 @@ class RouteConfiguration {
/// example.
final Codec<Object?, Object?>? extraCodec;

/// The GoRouter instance that owns this configuration.
/// This is used to provide access to the router during redirects.
final GoRouter? router;

final Map<String, _NamedPath> _nameToPath = <String, _NamedPath>{};

/// Looks up the url location by a [GoRoute]'s name.
Expand Down Expand Up @@ -416,10 +424,12 @@ class RouteConfiguration {

redirectHistory.add(prevMatchList);
// Check for top-level redirect
final FutureOr<String?> topRedirectResult = _routingConfig.value.redirect(
context,
buildTopLevelGoRouterState(prevMatchList),
);
final FutureOr<String?> topRedirectResult = _runInRouterZone(() {
return _routingConfig.value.redirect(
context,
buildTopLevelGoRouterState(prevMatchList),
);
});

if (topRedirectResult is String?) {
return processTopLevelRedirect(topRedirectResult);
Expand Down Expand Up @@ -448,10 +458,12 @@ class RouteConfiguration {
_getRouteLevelRedirect(
context, matchList, routeMatches, currentCheckIndex + 1);
final RouteBase route = match.route;
final FutureOr<String?> routeRedirectResult = route.redirect!.call(
context,
match.buildState(this, matchList),
);
final FutureOr<String?> routeRedirectResult = _runInRouterZone(() {
return route.redirect!.call(
context,
match.buildState(this, matchList),
);
});
if (routeRedirectResult is String?) {
return processRouteRedirect(routeRedirectResult);
}
Expand Down Expand Up @@ -508,6 +520,20 @@ class RouteConfiguration {
.join(' => ');
}

/// Runs the given function in a Zone with the router context for redirects.
T _runInRouterZone<T>(T Function() callback) {
if (router == null) {
return callback();
}

return runZoned<T>(
callback,
zoneValues: <Object?, Object?>{
_currentRouterKey: router,
},
);
}

/// Get the location for the provided route.
///
/// Builds the absolute path for the route, by concatenating the paths of the
Expand Down
2 changes: 0 additions & 2 deletions packages/go_router/lib/src/misc/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import '../router.dart';
/// context.go('/');
extension GoRouterHelper on BuildContext {
/// Get a location from route name and parameters.
///
/// This method can't be called during redirects.
String namedLocation(
String name, {
Map<String, String> pathParameters = const <String, String>{},
Expand Down
30 changes: 25 additions & 5 deletions packages/go_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import 'parser.dart';
import 'route.dart';
import 'state.dart';

/// Symbol used as a Zone key to track the current GoRouter during redirects.
const Symbol _currentRouterKey = #goRouterRedirectContext;

/// The function signature of [GoRouter.onException].
///
/// Use `state.error` to access the exception.
Expand Down Expand Up @@ -206,6 +209,7 @@ class GoRouter implements RouterConfig<RouteMatchList> {
_routingConfig,
navigatorKey: navigatorKey,
extraCodec: extraCodec,
router: this,
);

final ParserExceptionHandler? parserExceptionHandler;
Expand Down Expand Up @@ -519,21 +523,37 @@ class GoRouter implements RouterConfig<RouteMatchList> {

/// Find the current GoRouter in the widget tree.
///
/// This method throws when it is called during redirects.
/// This method can now be called during redirects.
static GoRouter of(BuildContext context) {
final GoRouter? inherited = maybeOf(context);
assert(inherited != null, 'No GoRouter found in context');
return inherited!;
if (inherited != null) {
return inherited;
}

// Check if we're in a redirect context
final GoRouter? redirectRouter =
Zone.current[_currentRouterKey] as GoRouter?;
if (redirectRouter != null) {
return redirectRouter;
}

throw FlutterError('No GoRouter found in context');
}

/// The current GoRouter in the widget tree, if any.
///
/// This method returns null when it is called during redirects.
/// This method can now return a router even during redirects.
static GoRouter? maybeOf(BuildContext context) {
final InheritedGoRouter? inherited = context
.getElementForInheritedWidgetOfExactType<InheritedGoRouter>()
?.widget as InheritedGoRouter?;
return inherited?.goRouter;

if (inherited != null) {
return inherited.goRouter;
}

// Check if we're in a redirect context
return Zone.current[_currentRouterKey] as GoRouter?;
}

/// Disposes resource created by this object.
Expand Down
55 changes: 55 additions & 0 deletions packages/go_router/test/go_router_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2238,6 +2238,61 @@ void main() {
expect(redirected, isTrue);
});

testWidgets('GoRouter.of(context) should work in redirects',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test that the error throw during the redirect can be caught by onException?

Copy link
Contributor Author

@tomassasovsky tomassasovsky Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added proper exception handling in the _redirect method to ensure redirect errors are gracefully converted to error RouteMatchList objects instead of crashing navigation.

What changed:

  • Wrapped redirect calls in try-catch for sync exceptions
  • Added .catchError() for async redirect exceptions
  • Both paths convert exceptions to GoException and return error match lists

Why this matters:
This ensures that when a redirect throws an exception (like in the failing test), it gets properly handled by the onException callback instead of breaking the entire navigation flow. Previously, exceptions would bubble up and crash the router - now they're caught and transformed into proper error states that the router knows how to handle.

Works hand-in-hand with the configuration.dart fix to provide complete exception handling coverage throughout the redirect chain.

(WidgetTester tester) async {
GoRouter? capturedRouter;
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
),
GoRoute(
path: '/login',
builder: (BuildContext context, GoRouterState state) =>
const LoginScreen(),
),
];

final GoRouter router = await createRouter(routes, tester,
redirect: (BuildContext context, GoRouterState state) {
// This should not throw an exception
capturedRouter = GoRouter.of(context);
return state.matchedLocation == '/login' ? null : '/login';
});

expect(capturedRouter, isNotNull);
expect(capturedRouter, equals(router));
});

testWidgets('Context extension methods should work in redirects',
(WidgetTester tester) async {
String? capturedNamedLocation;
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
name: 'home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
),
GoRoute(
path: '/login',
name: 'login',
builder: (BuildContext context, GoRouterState state) =>
const LoginScreen(),
),
];

await createRouter(routes, tester,
redirect: (BuildContext context, GoRouterState state) {
// This should not throw an exception
capturedNamedLocation = context.namedLocation('login');
return state.matchedLocation == '/login' ? null : '/login';
});

expect(capturedNamedLocation, '/login');
});

testWidgets('redirect can redirect to same path',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
Expand Down