[go_router] Fix assertion failure on URLs with hash fragments missing leading slash (#11431)
Fixes https://github.com/flutter/flutter/issues/184109
Fixes https://github.com/flutter/flutter/issues/181629
diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart
index e78011f..15c159a 100644
--- a/packages/go_router/lib/src/configuration.dart
+++ b/packages/go_router/lib/src/configuration.dart
@@ -260,12 +260,17 @@
/// Normalizes a URI by ensuring it has a valid path and removing trailing slashes.
static Uri normalizeUri(Uri uri) {
- if (uri.hasEmptyPath) {
- return uri.replace(path: '/');
- } else if (uri.path.length > 1 && uri.path.endsWith('/')) {
- return uri.replace(path: uri.path.substring(0, uri.path.length - 1));
+ String path = uri.path;
+ if (!path.startsWith('/')) {
+ path = '/$path';
}
- return uri;
+ if (path.length > 1 && path.endsWith('/')) {
+ path = path.substring(0, path.length - 1);
+ }
+ if (path == uri.path) {
+ return uri;
+ }
+ return uri.replace(path: path);
}
/// The global key for top level navigator.
diff --git a/packages/go_router/pending_changelogs/fix_assertion_missing_slash.yaml b/packages/go_router/pending_changelogs/fix_assertion_missing_slash.yaml
new file mode 100644
index 0000000..378fbff
--- /dev/null
+++ b/packages/go_router/pending_changelogs/fix_assertion_missing_slash.yaml
@@ -0,0 +1,3 @@
+changelog: |
+ - Fixes an assertion failure when navigating to URLs with hash fragments missing a leading slash.
+version: patch
diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart
index 88185f0..2e6e9cc 100644
--- a/packages/go_router/test/configuration_test.dart
+++ b/packages/go_router/test/configuration_test.dart
@@ -1010,6 +1010,54 @@
);
},
);
+
+ group('normalizeUri', () {
+ test('adds leading slash if missing', () {
+ expect(RouteConfiguration.normalizeUri(Uri.parse('foo')).path, '/foo');
+ });
+
+ test('handles empty path', () {
+ expect(RouteConfiguration.normalizeUri(Uri.parse('')).path, '/');
+ });
+
+ test('removes trailing slash if length > 1', () {
+ expect(
+ RouteConfiguration.normalizeUri(Uri.parse('/foo/')).path,
+ '/foo',
+ );
+ });
+
+ test('does not remove slash for root root', () {
+ expect(RouteConfiguration.normalizeUri(Uri.parse('/')).path, '/');
+ });
+
+ test('preserves query parameters and fragments', () {
+ final Uri uri = RouteConfiguration.normalizeUri(Uri.parse('foo?a=b#c'));
+ expect(uri.path, '/foo');
+ expect(uri.queryParameters['a'], 'b');
+ expect(uri.fragment, 'c');
+ });
+
+ test('handles hash fragments with authority', () {
+ final Uri uri = RouteConfiguration.normalizeUri(
+ Uri.parse('http://localhost:3000/#foo'),
+ );
+ expect(uri.path, '/');
+ expect(uri.fragment, 'foo');
+ });
+
+ test('handles hash fragments without authority', () {
+ final Uri uri = RouteConfiguration.normalizeUri(Uri.parse('/#foo'));
+ expect(uri.path, '/');
+ expect(uri.fragment, 'foo');
+ });
+
+ test('returns same instance if already normalized', () {
+ final Uri uri = Uri.parse('/foo');
+ final Uri normalized = RouteConfiguration.normalizeUri(uri);
+ expect(identical(uri, normalized), isTrue);
+ });
+ });
});
}
diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart
index 3859f27..d6d4d18 100644
--- a/packages/go_router/test/parser_test.dart
+++ b/packages/go_router/test/parser_test.dart
@@ -662,4 +662,30 @@
expect(match.matches, hasLength(1));
expect(matchesObj.error, isNull);
});
+
+ testWidgets(
+ 'GoRouteInformationParser can handle path without leading slash',
+ (WidgetTester tester) async {
+ final routes = <RouteBase>[
+ GoRoute(path: '/abc', builder: (_, __) => const Placeholder()),
+ ];
+ final GoRouteInformationParser parser = await createParser(
+ tester,
+ routes: routes,
+ redirectLimit: 100,
+ redirect: (_, __) => null,
+ );
+
+ final BuildContext context = tester.element(find.byType(Router<Object>));
+
+ final RouteMatchList matchesObj = await parser
+ .parseRouteInformationWithDependencies(
+ createRouteInformation('abc'),
+ context,
+ );
+ final List<RouteMatchBase> matches = matchesObj.matches;
+ expect(matches.length, 1);
+ expect(matchesObj.uri.toString(), '/abc');
+ },
+ );
}