Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
5 changes: 5 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 16.0.1

- Fixes `GoRouter.of(context)` access inside redirect callbacks by providing router access through Zone-based context tracking.
- Adds support for using context extension methods (e.g., `context.namedLocation()`, `context.go()`) within redirect callbacks.

## 16.0.0

- **BREAKING CHANGE**
Expand Down
63 changes: 55 additions & 8 deletions packages/go_router/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart';

import 'logging.dart';
import 'match.dart';
import 'misc/constants.dart';
import 'misc/errors.dart';
import 'path_utils.dart';
import 'route.dart';
Expand All @@ -29,6 +30,7 @@ class RouteConfiguration {
this._routingConfig, {
required this.navigatorKey,
this.extraCodec,
this.router,
}) {
_onRoutingTableChanged();
_routingConfig.addListener(_onRoutingTableChanged);
Expand Down Expand Up @@ -248,6 +250,11 @@ 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 +423,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 +457,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 +519,42 @@ 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,
},
zoneSpecification: ZoneSpecification(
handleUncaughtError: (
Zone zone,
ZoneDelegate zoneDelegate,
Zone zone2,
Object error,
StackTrace stackTrace,
) {
// Handle errors by routing them to GoRouter's error handling mechanisms
if (error is GoException) {
// For GoException, we can let it propagate as it's already handled
// by the existing error handling in redirect methods
throw error;
} else {
// For other errors, we should route them to the router's error handling
// This will be handled by GoRouter.onException or error builders
log('Error in router zone: $error');
// Convert the error to a GoException for proper error handling
throw GoException('error in router zone: $error');
}
},
),
);
}

/// Get the location for the provided route.
///
/// Builds the absolute path for the route, by concatenating the paths of the
Expand Down
5 changes: 5 additions & 0 deletions packages/go_router/lib/src/misc/constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'package:meta/meta.dart';

/// Symbol used as a Zone key to track the current GoRouter during redirects.
@internal
const Symbol currentRouterKey = #goRouterRedirectContext;
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
22 changes: 14 additions & 8 deletions packages/go_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'delegate.dart';
import 'information_provider.dart';
import 'logging.dart';
import 'match.dart';
import 'misc/constants.dart';
import 'misc/inherited_router.dart';
import 'parser.dart';
import 'route.dart';
Expand Down Expand Up @@ -206,6 +207,7 @@ class GoRouter implements RouterConfig<RouteMatchList> {
_routingConfig,
navigatorKey: navigatorKey,
extraCodec: extraCodec,
router: this,
);

final ParserExceptionHandler? parserExceptionHandler;
Expand Down Expand Up @@ -518,22 +520,26 @@ class GoRouter implements RouterConfig<RouteMatchList> {
}

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

/// The current GoRouter in the widget tree, if any.
///
/// This method returns null when it is called 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
2 changes: 1 addition & 1 deletion packages/go_router/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
version: 16.0.0
version: 16.0.1
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

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