Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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