Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
172 changes: 111 additions & 61 deletions packages/go_router/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ class RouteConfiguration {

/// Legacy top level page redirect.
///
/// This is handled via [applyTopLegacyRedirect] and runs at most once per navigation.
/// This is handled via [applyTopLegacyRedirect] inside [redirect].
GoRouterRedirect get topRedirect => _routingConfig.value.redirect;

/// Top level page on enter.
Expand Down Expand Up @@ -401,80 +401,121 @@ class RouteConfiguration {
return const <RouteMatchBase>[];
}

/// Processes route-level redirects by returning a new [RouteMatchList] representing the new location.
/// Processes all redirects (top-level and route-level) and returns the
/// final [RouteMatchList].
///
/// This method now handles ONLY route-level redirects.
/// Top-level redirects are handled by applyTopLegacyRedirect.
/// Top-level redirects are evaluated first via [applyTopLegacyRedirect],
/// then route-level redirects run on the resulting match list. If a
/// route-level redirect changes the location, this method recurses —
/// re-evaluating top-level redirects on the new location naturally.
FutureOr<RouteMatchList> redirect(
BuildContext context,
FutureOr<RouteMatchList> prevMatchListFuture, {
required List<RouteMatchList> redirectHistory,
}) {
FutureOr<RouteMatchList> processRedirect(RouteMatchList prevMatchList) {
final prevLocation = prevMatchList.uri.toString();

FutureOr<RouteMatchList> processRouteLevelRedirect(
String? routeRedirectLocation,
) {
if (routeRedirectLocation != null &&
routeRedirectLocation != prevLocation) {
final RouteMatchList newMatch = _getNewMatches(
routeRedirectLocation,
prevMatchList.uri,
redirectHistory,
);
// Step 1: Apply top-level redirect first (self-recursive for chains).
final FutureOr<RouteMatchList> afterTopLevel = applyTopLegacyRedirect(
context,
prevMatchList,
redirectHistory: redirectHistory,
);

if (newMatch.isError) {
return newMatch;
}
return redirect(context, newMatch, redirectHistory: redirectHistory);
}
return prevMatchList;
// Step 2: Then apply route-level redirects on the post-top-level result.
if (afterTopLevel is RouteMatchList) {
return _processRouteLevelRedirects(
context,
afterTopLevel,
redirectHistory,
);
}

final routeMatches = <RouteMatchBase>[];
prevMatchList.visitRouteMatches((RouteMatchBase match) {
if (match.route.redirect != null) {
routeMatches.add(match);
return afterTopLevel.then<RouteMatchList>((RouteMatchList ml) {
if (!context.mounted) {
return ml;
}
return true;
return _processRouteLevelRedirects(context, ml, redirectHistory);
});
}

if (prevMatchListFuture is RouteMatchList) {
return processRedirect(prevMatchListFuture);
}
return prevMatchListFuture.then<RouteMatchList>(processRedirect);
}

try {
final FutureOr<String?> routeLevelRedirectResult =
_getRouteLevelRedirect(context, prevMatchList, routeMatches, 0);
/// Processes route-level redirects on [matchList].
///
/// If a route-level redirect changes the location, recurses back into
/// [redirect] which will re-evaluate top-level redirects on the new path.
FutureOr<RouteMatchList> _processRouteLevelRedirects(
BuildContext context,
RouteMatchList matchList,
List<RouteMatchList> redirectHistory,
) {
final prevLocation = matchList.uri.toString();

if (routeLevelRedirectResult is String?) {
return processRouteLevelRedirect(routeLevelRedirectResult);
}
return routeLevelRedirectResult
.then<RouteMatchList>(processRouteLevelRedirect)
.catchError((Object error) {
final GoException goException = error is GoException
? error
: GoException('Exception during route redirect: $error');
return _errorRouteMatchList(
prevMatchList.uri,
goException,
extra: prevMatchList.extra,
);
});
} catch (exception) {
final GoException goException = exception is GoException
? exception
: GoException('Exception during route redirect: $exception');
return _errorRouteMatchList(
prevMatchList.uri,
goException,
extra: prevMatchList.extra,
FutureOr<RouteMatchList> processRouteLevelRedirect(
String? routeRedirectLocation,
) {
if (routeRedirectLocation != null &&
routeRedirectLocation != prevLocation) {
final RouteMatchList newMatch = _getNewMatches(
routeRedirectLocation,
matchList.uri,
redirectHistory,
);

if (newMatch.isError) {
return newMatch;
}
// Recurse into redirect — top-level will be re-evaluated at the
// top of the next cycle.
return redirect(context, newMatch, redirectHistory: redirectHistory);
}
return matchList;
}

if (prevMatchListFuture is RouteMatchList) {
return processRedirect(prevMatchListFuture);
final routeMatches = <RouteMatchBase>[];
matchList.visitRouteMatches((RouteMatchBase match) {
if (match.route.redirect != null) {
routeMatches.add(match);
}
return true;
});

try {
final FutureOr<String?> routeLevelRedirectResult = _getRouteLevelRedirect(
context,
matchList,
routeMatches,
0,
);

if (routeLevelRedirectResult is String?) {
return processRouteLevelRedirect(routeLevelRedirectResult);
}
return routeLevelRedirectResult
.then<RouteMatchList>(processRouteLevelRedirect)
.catchError((Object error) {
final GoException goException = error is GoException
? error
: GoException('Exception during route redirect: $error');
return _errorRouteMatchList(
matchList.uri,
goException,
extra: matchList.extra,
);
});
} catch (exception) {
final GoException goException = exception is GoException
? exception
: GoException('Exception during route redirect: $exception');
return _errorRouteMatchList(
matchList.uri,
goException,
extra: matchList.extra,
);
}
return prevMatchListFuture.then<RouteMatchList>(processRedirect);
}

/// Applies the legacy top-level redirect to [prevMatchList] and returns the
Expand All @@ -484,9 +525,10 @@ class RouteConfiguration {
///
/// Shares [redirectHistory] with later route-level redirects for proper loop detection.
///
/// Note: Legacy top-level redirect is executed at most once per navigation,
/// before route-level redirects. It does not re-evaluate if it redirects to
/// a location that would itself trigger another top-level redirect.
/// Recursively re-evaluates the top-level redirect when it produces a new
/// location, so that top-level redirect chains (e.g. `/` → `/a` → `/b`) are
/// fully resolved. Loop detection and redirect limit are enforced via the
/// shared [redirectHistory].
FutureOr<RouteMatchList> applyTopLegacyRedirect(
BuildContext context,
RouteMatchList prevMatchList, {
Expand All @@ -500,7 +542,15 @@ class RouteConfiguration {
prevMatchList.uri,
redirectHistory,
);
return newMatch;
if (newMatch.isError) {
return newMatch;
}
// Recursively re-evaluate the top-level redirect on the new location.
return applyTopLegacyRedirect(
context,
newMatch,
redirectHistory: redirectHistory,
);
}
return prevMatchList;
}
Expand Down
74 changes: 23 additions & 51 deletions packages/go_router/lib/src/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,54 +111,22 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
);

// ALL navigation types now go through onEnter, and if allowed,
// legacy top-level redirect runs, then route-level redirects.
// redirect() handles both top-level and route-level redirects.
return _onEnterHandler.handleTopOnEnter(
context: context,
routeInformation: effectiveRoute,
infoState: infoState,
onCanEnter: () {
// Compose legacy top-level redirect here (one shared cycle/history).
final RouteMatchList initialMatches = configuration.findMatch(
effectiveRoute.uri,
extra: infoState.extra,
);
final redirectHistory = <RouteMatchList>[];

final FutureOr<RouteMatchList> afterLegacy = configuration
.applyTopLegacyRedirect(
context,
initialMatches,
redirectHistory: redirectHistory,
);

if (afterLegacy is RouteMatchList) {
return _navigate(
effectiveRoute,
context,
infoState,
startingMatches: afterLegacy,
preSharedHistory: redirectHistory,
);
}
return afterLegacy.then((RouteMatchList ml) {
if (!context.mounted) {
return _lastMatchList ??
_OnEnterHandler._errorRouteMatchList(
effectiveRoute.uri,
GoException(
'Navigation aborted because the router context was disposed.',
),
extra: infoState.extra,
);
}
return _navigate(
effectiveRoute,
context,
infoState,
startingMatches: ml,
preSharedHistory: redirectHistory,
);
});
return _navigate(
effectiveRoute,
context,
infoState,
startingMatches: initialMatches,
);
},
onCanNotEnter: () {
// If blocked, stay on the current route by restoring the last known good configuration.
Expand Down Expand Up @@ -198,19 +166,17 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
BuildContext context,
RouteInfoState infoState, {
FutureOr<RouteMatchList>? startingMatches,
List<RouteMatchList>? preSharedHistory,
}) {
// If we weren't given matches, compute them here. The URI has already been
// normalized at the parser entry point.
final FutureOr<RouteMatchList> baseMatches =
startingMatches ??
configuration.findMatch(routeInformation.uri, extra: infoState.extra);

// History may be shared with the legacy step done in onEnter.
final List<RouteMatchList> redirectHistory =
preSharedHistory ?? <RouteMatchList>[];
final redirectHistory = <RouteMatchList>[];

FutureOr<RouteMatchList> afterRouteLevel(FutureOr<RouteMatchList> base) {
// redirect() handles both top-level and route-level redirects.
FutureOr<RouteMatchList> applyRedirects(FutureOr<RouteMatchList> base) {
if (base is RouteMatchList) {
return configuration.redirect(
context,
Expand All @@ -222,27 +188,33 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
if (!context.mounted) {
return ml;
}
final FutureOr<RouteMatchList> step = configuration.redirect(
return configuration.redirect(
context,
ml,
redirectHistory: redirectHistory,
);
return step;
});
}

// Only route-level redirects from here on out.
final FutureOr<RouteMatchList> redirected = afterRouteLevel(baseMatches);
final FutureOr<RouteMatchList> redirected = applyRedirects(baseMatches);

return debugParserFuture =
(redirected is RouteMatchList
? SynchronousFuture<RouteMatchList>(redirected)
: redirected)
.then((RouteMatchList matchList) {
// Guard against context disposal during async redirects.
if (!context.mounted) {
return _lastMatchList ??
_OnEnterHandler._errorRouteMatchList(
routeInformation.uri,
GoException(
'Navigation aborted because the router context was disposed.',
),
extra: infoState.extra,
);
}
if (matchList.isError && onParserException != null) {
if (!context.mounted) {
return matchList;
}
return onParserException!(context, matchList);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changelog: |
- Fixes chained top-level redirects not being fully resolved (e.g. `/ → /a → /b` stopping at `/a`).
- Fixes route-level redirects not triggering top-level redirect re-evaluation on the new location.
version: patch
Loading
Loading