blob: 29b541d786c2615f2c7c3cbfb2afa7a08c39efb4 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'configuration.dart';
import 'logging.dart';
import 'match.dart';
import 'matching.dart';
/// A GoRouter redirector function.
// TODO(johnpryan): make redirector async
// See
typedef RouteRedirector = RouteMatchList Function(RouteMatchList matches,
RouteConfiguration configuration, RouteMatcher matcher,
{Object? extra});
/// Processes redirects by returning a new [RouteMatchList] representing the new
/// location.
RouteMatchList redirect(RouteMatchList prevMatchList,
RouteConfiguration configuration, RouteMatcher matcher,
{Object? extra}) {
RouteMatchList matches;
// Store each redirect to detect loops
final List<RouteMatchList> redirects = <RouteMatchList>[prevMatchList];
// Keep looping until redirecting is done
while (true) {
final RouteMatchList currentMatches = redirects.last;
// Check for top-level redirect
final Uri uri = currentMatches.location;
final String? topRedirectLocation = configuration.topRedirect(
location: currentMatches.location.toString(),
name: null,
// No name available at the top level trim the query params off the
// sub-location to match route.redirect
subloc: uri.path,
queryParams: uri.queryParameters,
extra: extra,
if (topRedirectLocation != null) {
final RouteMatchList newMatch = matcher.findMatch(topRedirectLocation);
_addRedirect(redirects, newMatch, prevMatchList.location,
// If there's no top-level redirect, keep the matches the same as before.
matches = currentMatches;
// Merge new params to keep params from previously matched paths, e.g.
// /users/:userId/book/:bookId provides userId and bookId to book/:bookId
Map<String, String> previouslyMatchedParams = <String, String>{};
for (final RouteMatch match in currentMatches.matches) {
'Duplicated parameter names',
previouslyMatchedParams = match.encodedParams;
// check top route for redirect
final RouteMatch? top = matches.isNotEmpty ? matches.last : null;
if (top == null) {
final String? topRouteLocation = top.route.redirect(
location: currentMatches.location.toString(),
subloc: top.subloc,
path: top.route.path,
fullpath: top.fullpath,
params: top.decodedParams,
queryParams: top.queryParams,
if (topRouteLocation == null) {
final RouteMatchList newMatchList = matcher.findMatch(topRouteLocation);
_addRedirect(redirects, newMatchList, prevMatchList.location,
return matches;
/// A configuration error detected while processing redirects.
class RedirectionError extends Error implements UnsupportedError {
/// RedirectionError constructor.
RedirectionError(this.message, this.matches, this.location);
/// The matches that were found while processing redirects.
final List<RouteMatchList> matches;
final String message;
/// The location that was originally navigated to, before redirection began.
final Uri location;
String toString() =>
super.toString() +
' ' +
(RouteMatchList routeMatches) => routeMatches.location.toString()),
].join(' => ');
/// Adds the redirect to [redirects] if it is valid.
void _addRedirect(List<RouteMatchList> redirects, RouteMatchList newMatch,
Uri prevLocation, int redirectLimit) {
// Verify that the redirect can be parsed and is not already
// in the list of redirects
assert(() {
if (redirects.contains(newMatch)) {
throw RedirectionError('redirect loop detected',
<RouteMatchList>[...redirects, newMatch], prevLocation);
if (redirects.length > redirectLimit) {
throw RedirectionError('too many redirects',
<RouteMatchList>[...redirects, newMatch], prevLocation);
return true;
assert(() {'redirecting to $newMatch');
return true;