Skip to content

Commit 8b1c605

Browse files
kforjanTecHaxter
authored andcommitted
[go_router] Fixes deep links with no path (flutter#6447)
This PR addresses an issue where root deep links did not correctly navigate to '/'. The problem originated from the assumption that only paths (like '/', '/details', etc.) would be passed to the `canonicalUri` method in `path_utils.dart`. However, in the case of deep links, the full URL (including the scheme and domain) would be passed, causing the trailing slash to be incorrectly removed for root URLs. The first part of the fix modifies the `canonicalUri` method to check the actual path using `uri.path`. This ensures that the trailing slash is preserved for root URLs, even when the full URL is passed to the method. Importantly, even if '/' or '/details' is passed as a URI to `canonicalUri`, `uri.path` will still return '/' or '/details', ensuring consistent behavior. The second part of the fix modifies the `parseRouteInformationWithDependencies` function in `parser.dart` to correctly handle URLs with query parameters or fragments. Previously, the trailing slash was added after the query parameters or fragments, which is incorrect. The fix ensures that the trailing slash is added immediately after the base URL, before any query parameters or fragments. Preserving the trailing slash for root URLs is crucial because the `_matchByNavigatorKey` method in `match.dart` uses `uri.path` as the `remainingLocation` parameter. (why is that method relevant? because `parseRouteInformationWithDependencies` uses `findMatch`, which calls `_getLocRouteMatches`, which calls `match` and it calls `_matchByNavigatorKey`). If the trailing slash is removed from the root deep link URL, `uri.path` will not return '/', and the URL will not match any routes in the Go router. To validate these changes, new tests have been added. In `path_utils_test.dart`, tests have been added to verify that the trailing slash is not removed from a URL that is not just the path. In `parser_test.dart`, a new test has been added to verify that a URI with an empty path is correctly parsed. This test covers the case where `routeInformation.uri.hasEmptyPath` is true, which was not previously tested. Issues fixed: - flutter/flutter#133928
1 parent 0934981 commit 8b1c605

File tree

6 files changed

+83
-11
lines changed

6 files changed

+83
-11
lines changed

packages/go_router/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 13.2.3
2+
3+
- Fixes an issue where deep links without path caused an exception
4+
15
## 13.2.2
26

37
- Fixes restoreRouteInformation issue when GoRouter.optionURLReflectsImperativeAPIs is true and the last match is ShellRouteMatch

packages/go_router/lib/src/parser.dart

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,24 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
7979
}
8080

8181
late final RouteMatchList initialMatches;
82-
initialMatches = configuration.findMatch(
83-
routeInformation.uri.path.isEmpty
84-
? '${routeInformation.uri}/'
85-
: routeInformation.uri.toString(),
86-
extra: state.extra);
82+
if (routeInformation.uri.hasEmptyPath) {
83+
String newUri = '${routeInformation.uri.origin}/';
84+
if (routeInformation.uri.hasQuery) {
85+
newUri += '?${routeInformation.uri.query}';
86+
}
87+
if (routeInformation.uri.hasFragment) {
88+
newUri += '#${routeInformation.uri.fragment}';
89+
}
90+
initialMatches = configuration.findMatch(
91+
newUri,
92+
extra: state.extra,
93+
);
94+
} else {
95+
initialMatches = configuration.findMatch(
96+
routeInformation.uri.toString(),
97+
extra: state.extra,
98+
);
99+
}
87100
if (initialMatches.isError) {
88101
log('No initial matches: ${routeInformation.uri.path}');
89102
}

packages/go_router/lib/src/path_utils.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,22 @@ String canonicalUri(String loc) {
126126
}
127127
String canon = Uri.parse(loc).toString();
128128
canon = canon.endsWith('?') ? canon.substring(0, canon.length - 1) : canon;
129+
final Uri uri = Uri.parse(canon);
129130

130131
// remove trailing slash except for when you shouldn't, e.g.
131132
// /profile/ => /profile
132133
// / => /
133-
// /login?from=/ => login?from=/
134-
canon = canon.endsWith('/') && canon != '/' && !canon.contains('?')
134+
// /login?from=/ => /login?from=/
135+
canon = uri.path.endsWith('/') &&
136+
uri.path != '/' &&
137+
!uri.hasQuery &&
138+
!uri.hasFragment
135139
? canon.substring(0, canon.length - 1)
136140
: canon;
137141

138142
// replace '/?', except for first occurrence, from path only
139143
// /login/?from=/ => /login?from=/
140144
// /?from=/ => /?from=/
141-
final Uri uri = Uri.parse(canon);
142145
final int pathStartIndex = uri.host.isNotEmpty
143146
? uri.toString().indexOf(uri.host) + uri.host.length
144147
: uri.hasScheme

packages/go_router/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: go_router
22
description: A declarative router for Flutter based on Navigation 2 supporting
33
deep linking, data-driven routes and more
4-
version: 13.2.2
4+
version: 13.2.3
55
repository: https://github.com/flutter/packages/tree/main/packages/go_router
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
77

packages/go_router/test/parser_test.dart

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,54 @@ void main() {
8282
});
8383

8484
testWidgets(
85-
"GoRouteInformationParser can parse deeplink route and maintain uri's scheme and host",
85+
"GoRouteInformationParser can parse deeplink root route and maintain uri's scheme, host, query and fragment",
86+
(WidgetTester tester) async {
87+
const String expectedScheme = 'https';
88+
const String expectedHost = 'www.example.com';
89+
const String expectedQuery = 'abc=def';
90+
const String expectedFragment = 'abc';
91+
const String expectedUriString =
92+
'$expectedScheme://$expectedHost/?$expectedQuery#$expectedFragment';
93+
final List<GoRoute> routes = <GoRoute>[
94+
GoRoute(
95+
path: '/',
96+
builder: (_, __) => const Placeholder(),
97+
),
98+
];
99+
final GoRouteInformationParser parser = await createParser(
100+
tester,
101+
routes: routes,
102+
redirectLimit: 100,
103+
redirect: (_, __) => null,
104+
);
105+
106+
final BuildContext context = tester.element(find.byType(Router<Object>));
107+
108+
final RouteMatchList matchesObj =
109+
await parser.parseRouteInformationWithDependencies(
110+
createRouteInformation(expectedUriString), context);
111+
final List<RouteMatchBase> matches = matchesObj.matches;
112+
expect(matches.length, 1);
113+
expect(matchesObj.uri.toString(), expectedUriString);
114+
expect(matchesObj.uri.scheme, expectedScheme);
115+
expect(matchesObj.uri.host, expectedHost);
116+
expect(matchesObj.uri.query, expectedQuery);
117+
expect(matchesObj.uri.fragment, expectedFragment);
118+
119+
expect(matches[0].matchedLocation, '/');
120+
expect(matches[0].route, routes[0]);
121+
});
122+
123+
testWidgets(
124+
"GoRouteInformationParser can parse deeplink route with a path and maintain uri's scheme, host, query and fragment",
86125
(WidgetTester tester) async {
87126
const String expectedScheme = 'https';
88127
const String expectedHost = 'www.example.com';
89128
const String expectedPath = '/abc';
129+
const String expectedQuery = 'abc=def';
130+
const String expectedFragment = 'abc';
90131
const String expectedUriString =
91-
'$expectedScheme://$expectedHost$expectedPath';
132+
'$expectedScheme://$expectedHost$expectedPath?$expectedQuery#$expectedFragment';
92133
final List<GoRoute> routes = <GoRoute>[
93134
GoRoute(
94135
path: '/',
@@ -119,6 +160,8 @@ void main() {
119160
expect(matchesObj.uri.scheme, expectedScheme);
120161
expect(matchesObj.uri.host, expectedHost);
121162
expect(matchesObj.uri.path, expectedPath);
163+
expect(matchesObj.uri.query, expectedQuery);
164+
expect(matchesObj.uri.fragment, expectedFragment);
122165

123166
expect(matches[0].matchedLocation, '/');
124167
expect(matches[0].route, routes[0]);

packages/go_router/test/path_utils_test.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ void main() {
100100
verify('/a/', '/a');
101101
verify('/', '/');
102102
verify('/a/b/', '/a/b');
103+
verify('https://www.example.com/', 'https://www.example.com/');
104+
verify('https://www.example.com/a', 'https://www.example.com/a');
105+
verify('https://www.example.com/a/', 'https://www.example.com/a');
106+
verify('https://www.example.com/a/b/', 'https://www.example.com/a/b');
107+
verify('https://www.example.com/?', 'https://www.example.com/');
108+
verify('https://www.example.com/?a=b', 'https://www.example.com/?a=b');
109+
verify('https://www.example.com/?a=/', 'https://www.example.com/?a=/');
110+
verify('https://www.example.com/a/?b=c', 'https://www.example.com/a?b=c');
111+
verify('https://www.example.com/#a/', 'https://www.example.com/#a/');
103112

104113
expect(() => canonicalUri('::::'), throwsA(isA<FormatException>()));
105114
expect(() => canonicalUri(''), throwsA(anything));

0 commit comments

Comments
 (0)