From 7fa4f4b80c18abd3b187c24c9da5271913e12e6c Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 3 Apr 2026 12:45:31 -0700 Subject: [PATCH 1/2] [go_router] Fix assertion failure on URLs with hash fragments missing leading slash Fixes https://github.com/flutter/flutter/issues/184109 --- packages/go_router/lib/src/configuration.dart | 12 +++--- .../fix_assertion_missing_slash.yaml | 3 ++ .../go_router/test/configuration_test.dart | 37 +++++++++++++++++++ packages/go_router/test/parser_test.dart | 29 +++++++++++++++ 4 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 packages/go_router/pending_changelogs/fix_assertion_missing_slash.yaml diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index ef408b442708..6a3c5a2aa839 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -260,12 +260,14 @@ class RouteConfiguration { /// 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); + } + 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 000000000000..378fbfff1ce0 --- /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 88185f026c12..7d9350b110c5 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -1010,6 +1010,43 @@ void main() { ); }, ); + + 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'); + }); + }); }); } diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart index 3859f272f8f5..c55e84230892 100644 --- a/packages/go_router/test/parser_test.dart +++ b/packages/go_router/test/parser_test.dart @@ -662,4 +662,33 @@ void main() { expect(match.matches, hasLength(1)); expect(matchesObj.error, isNull); }); + + testWidgets( + 'GoRouteInformationParser can handle path without leading slash', + (WidgetTester tester) async { + final routes = [ + 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)); + + final RouteMatchList matchesObj = await parser + .parseRouteInformationWithDependencies( + createRouteInformation('abc'), + context, + ); + final List matches = matchesObj.matches; + expect(matches.length, 1); + expect(matchesObj.uri.toString(), '/abc'); + }, + ); } From f5a9d3a2b03495ec02ca0f0d7f208aca414ba94b Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 3 Apr 2026 13:12:39 -0700 Subject: [PATCH 2/2] Optimize normalizeUri and add authority hash tests --- packages/go_router/lib/src/configuration.dart | 3 ++ .../go_router/test/configuration_test.dart | 35 ++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 6a3c5a2aa839..0cc36ee5b85c 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -267,6 +267,9 @@ class RouteConfiguration { if (path.length > 1 && path.endsWith('/')) { path = path.substring(0, path.length - 1); } + if (path == uri.path) { + return uri; + } return uri.replace(path: path); } diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 7d9350b110c5..2e6e9cc39321 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -1013,17 +1013,11 @@ void main() { group('normalizeUri', () { test('adds leading slash if missing', () { - expect( - RouteConfiguration.normalizeUri(Uri.parse('foo')).path, - '/foo', - ); + expect(RouteConfiguration.normalizeUri(Uri.parse('foo')).path, '/foo'); }); test('handles empty path', () { - expect( - RouteConfiguration.normalizeUri(Uri.parse('')).path, - '/', - ); + expect(RouteConfiguration.normalizeUri(Uri.parse('')).path, '/'); }); test('removes trailing slash if length > 1', () { @@ -1034,10 +1028,7 @@ void main() { }); test('does not remove slash for root root', () { - expect( - RouteConfiguration.normalizeUri(Uri.parse('/')).path, - '/', - ); + expect(RouteConfiguration.normalizeUri(Uri.parse('/')).path, '/'); }); test('preserves query parameters and fragments', () { @@ -1046,6 +1037,26 @@ void main() { 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); + }); }); }); }