Skip to content

Commit d848b16

Browse files
[go_router] Fix routing to treat URLs with different cases (e.g., /Home vs /home) as distinct routes. (#9426)
Fixes flutter/flutter#169809 ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 68e93c9 commit d848b16

File tree

9 files changed

+87
-34
lines changed

9 files changed

+87
-34
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+
## 15.2.4
2+
3+
- Fixes routing to treat URLs with different cases (e.g., `/Home` vs `/home`) as distinct routes.
4+
15
## 15.2.3
26

37
- Updates Type-safe routes topic documentation to use the mixin from `go_router_builder` 3.0.0.

packages/go_router/lib/src/configuration.dart

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import 'state.dart';
2020
typedef GoRouterRedirect = FutureOr<String?> Function(
2121
BuildContext context, GoRouterState state);
2222

23+
typedef _NamedPath = ({String path, bool caseSensitive});
24+
2325
/// The route configuration for GoRouter configured by the app.
2426
class RouteConfiguration {
2527
/// Constructs a [RouteConfiguration].
@@ -246,7 +248,7 @@ class RouteConfiguration {
246248
/// example.
247249
final Codec<Object?, Object?>? extraCodec;
248250

249-
final Map<String, String> _nameToPath = <String, String>{};
251+
final Map<String, _NamedPath> _nameToPath = <String, _NamedPath>{};
250252

251253
/// Looks up the url location by a [GoRoute]'s name.
252254
String namedLocation(
@@ -264,11 +266,11 @@ class RouteConfiguration {
264266
return true;
265267
}());
266268
assert(_nameToPath.containsKey(name), 'unknown route name: $name');
267-
final String path = _nameToPath[name]!;
269+
final _NamedPath path = _nameToPath[name]!;
268270
assert(() {
269271
// Check that all required params are present
270272
final List<String> paramNames = <String>[];
271-
patternToRegExp(path, paramNames);
273+
patternToRegExp(path.path, paramNames, caseSensitive: path.caseSensitive);
272274
for (final String paramName in paramNames) {
273275
assert(pathParameters.containsKey(paramName),
274276
'missing param "$paramName" for $path');
@@ -284,7 +286,10 @@ class RouteConfiguration {
284286
for (final MapEntry<String, String> param in pathParameters.entries)
285287
param.key: Uri.encodeComponent(param.value)
286288
};
287-
final String location = patternToPath(path, encodedParams);
289+
final String location = patternToPath(
290+
path.path,
291+
encodedParams,
292+
);
288293
return Uri(
289294
path: location,
290295
queryParameters: queryParameters.isEmpty ? null : queryParameters,
@@ -528,8 +533,9 @@ class RouteConfiguration {
528533

529534
if (_nameToPath.isNotEmpty) {
530535
sb.writeln('known full paths for route names:');
531-
for (final MapEntry<String, String> e in _nameToPath.entries) {
532-
sb.writeln(' ${e.key} => ${e.value}');
536+
for (final MapEntry<String, _NamedPath> e in _nameToPath.entries) {
537+
sb.writeln(
538+
' ${e.key} => ${e.value.path}${e.value.caseSensitive ? '' : ' (case-insensitive)'}');
533539
}
534540
}
535541

@@ -594,8 +600,9 @@ class RouteConfiguration {
594600
assert(
595601
!_nameToPath.containsKey(name),
596602
'duplication fullpaths for name '
597-
'"$name":${_nameToPath[name]}, $fullPath');
598-
_nameToPath[name] = fullPath;
603+
'"$name":${_nameToPath[name]!.path}, $fullPath');
604+
_nameToPath[name] =
605+
(path: fullPath, caseSensitive: route.caseSensitive);
599606
}
600607

601608
if (route.routes.isNotEmpty) {

packages/go_router/lib/src/match.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,9 +660,20 @@ class RouteMatchList with Diagnosticable {
660660
matches: newMatches,
661661
);
662662
}
663+
664+
if (newMatches.isEmpty) {
665+
return RouteMatchList.empty;
666+
}
667+
668+
RouteBase newRoute = newMatches.last.route;
669+
while (newRoute is ShellRouteBase) {
670+
newRoute = newRoute.routes.last;
671+
}
672+
newRoute as GoRoute;
663673
// Need to remove path parameters that are no longer in the fullPath.
664674
final List<String> newParameters = <String>[];
665-
patternToRegExp(fullPath, newParameters);
675+
patternToRegExp(fullPath, newParameters,
676+
caseSensitive: newRoute.caseSensitive);
666677
final Set<String> validParameters = newParameters.toSet();
667678
final Map<String, String> newPathParameters =
668679
Map<String, String>.fromEntries(

packages/go_router/lib/src/path_utils.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?');
2323
/// To extract the path parameter values from a [RegExpMatch], pass the
2424
/// [RegExpMatch] into [extractPathParameters] with the `parameters` that are
2525
/// used for generating the [RegExp].
26-
RegExp patternToRegExp(String pattern, List<String> parameters) {
26+
RegExp patternToRegExp(String pattern, List<String> parameters,
27+
{required bool caseSensitive}) {
2728
final StringBuffer buffer = StringBuffer('^');
2829
int start = 0;
2930
for (final RegExpMatch match in _parameterRegExp.allMatches(pattern)) {
@@ -47,7 +48,7 @@ RegExp patternToRegExp(String pattern, List<String> parameters) {
4748
if (!pattern.endsWith('/')) {
4849
buffer.write(r'(?=/|$)');
4950
}
50-
return RegExp(buffer.toString(), caseSensitive: false);
51+
return RegExp(buffer.toString(), caseSensitive: caseSensitive);
5152
}
5253

5354
String _escapeGroup(String group, [String? name]) {

packages/go_router/lib/src/route.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ class GoRoute extends RouteBase {
285285
'if onExit is provided, one of pageBuilder or builder must be provided'),
286286
super._() {
287287
// cache the path regexp and parameters
288-
_pathRE = patternToRegExp(path, pathParameters);
288+
_pathRE =
289+
patternToRegExp(path, pathParameters, caseSensitive: caseSensitive);
289290
}
290291

291292
/// Whether this [GoRoute] only redirects to another route.
@@ -1193,7 +1194,8 @@ class StatefulNavigationShell extends StatefulWidget {
11931194
/// find the first GoRoute, from which a full path will be derived.
11941195
final GoRoute route = branch.defaultRoute!;
11951196
final List<String> parameters = <String>[];
1196-
patternToRegExp(route.path, parameters);
1197+
patternToRegExp(route.path, parameters,
1198+
caseSensitive: route.caseSensitive);
11971199
assert(parameters.isEmpty);
11981200
final String fullPath = _router.configuration.locationForRoute(route)!;
11991201
return patternToPath(

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: 15.2.3
4+
version: 15.2.4
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/go_router_test.dart

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
1313
import 'package:flutter_test/flutter_test.dart';
1414
import 'package:go_router/go_router.dart';
1515
import 'package:go_router/src/match.dart';
16+
import 'package:go_router/src/pages/material.dart';
1617
import 'package:logging/logging.dart';
1718

1819
import 'test_helpers.dart';
@@ -782,12 +783,6 @@ void main() {
782783
});
783784

784785
testWidgets('match path case sensitively', (WidgetTester tester) async {
785-
final FlutterExceptionHandler? oldFlutterError = FlutterError.onError;
786-
addTearDown(() => FlutterError.onError = oldFlutterError);
787-
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
788-
FlutterError.onError = (FlutterErrorDetails details) {
789-
errors.add(details);
790-
};
791786
final List<GoRoute> routes = <GoRoute>[
792787
GoRoute(
793788
path: '/',
@@ -804,16 +799,11 @@ void main() {
804799
final GoRouter router = await createRouter(routes, tester);
805800
const String wrongLoc = '/FaMiLy/f2';
806801

807-
expect(errors, isEmpty);
808802
router.go(wrongLoc);
809803
await tester.pumpAndSettle();
810804

811-
expect(errors, hasLength(1));
812-
expect(
813-
errors.single.exception,
814-
isAssertionError,
815-
reason: 'The path is case sensitive',
816-
);
805+
expect(find.byType(MaterialErrorScreen), findsOne);
806+
expect(find.text('Page Not Found'), findsOne);
817807

818808
const String loc = '/family/f2';
819809
router.go(loc);
@@ -827,8 +817,42 @@ void main() {
827817
);
828818

829819
expect(matches, hasLength(1));
830-
expect(find.byType(FamilyScreen), findsOneWidget);
831-
expect(errors, hasLength(1), reason: 'No new errors should be thrown');
820+
expect(find.byType(FamilyScreen), findsOne);
821+
});
822+
823+
testWidgets('supports routes with a different case',
824+
(WidgetTester tester) async {
825+
final List<GoRoute> routes = <GoRoute>[
826+
GoRoute(
827+
path: '/',
828+
builder: (BuildContext context, GoRouterState state) =>
829+
const HomeScreen(),
830+
),
831+
GoRoute(
832+
path: '/abc',
833+
builder: (BuildContext context, GoRouterState state) =>
834+
const SizedBox(key: Key('abc')),
835+
),
836+
GoRoute(
837+
path: '/ABC',
838+
builder: (BuildContext context, GoRouterState state) =>
839+
const SizedBox(key: Key('ABC')),
840+
),
841+
];
842+
843+
final GoRouter router = await createRouter(routes, tester);
844+
const String loc1 = '/abc';
845+
846+
router.go(loc1);
847+
await tester.pumpAndSettle();
848+
849+
expect(find.byKey(const Key('abc')), findsOne);
850+
851+
const String loc = '/ABC';
852+
router.go(loc);
853+
await tester.pumpAndSettle();
854+
855+
expect(find.byKey(const Key('ABC')), findsOne);
832856
});
833857

834858
testWidgets(

packages/go_router/test/path_utils_test.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ void main() {
99
test('patternToRegExp without path parameter', () async {
1010
const String pattern = '/settings/detail';
1111
final List<String> pathParameter = <String>[];
12-
final RegExp regex = patternToRegExp(pattern, pathParameter);
12+
final RegExp regex =
13+
patternToRegExp(pattern, pathParameter, caseSensitive: true);
1314
expect(pathParameter.isEmpty, isTrue);
1415
expect(regex.hasMatch('/settings/detail'), isTrue);
1516
expect(regex.hasMatch('/settings/'), isFalse);
@@ -22,7 +23,8 @@ void main() {
2223
test('patternToRegExp with path parameter', () async {
2324
const String pattern = '/user/:id/book/:bookId';
2425
final List<String> pathParameter = <String>[];
25-
final RegExp regex = patternToRegExp(pattern, pathParameter);
26+
final RegExp regex =
27+
patternToRegExp(pattern, pathParameter, caseSensitive: true);
2628
expect(pathParameter.length, 2);
2729
expect(pathParameter[0], 'id');
2830
expect(pathParameter[1], 'bookId');
@@ -44,7 +46,8 @@ void main() {
4446
test('patternToPath without path parameter', () async {
4547
const String pattern = '/settings/detail';
4648
final List<String> pathParameter = <String>[];
47-
final RegExp regex = patternToRegExp(pattern, pathParameter);
49+
final RegExp regex =
50+
patternToRegExp(pattern, pathParameter, caseSensitive: true);
4851

4952
const String url = '/settings/detail';
5053
final RegExpMatch? match = regex.firstMatch(url);
@@ -60,7 +63,8 @@ void main() {
6063
test('patternToPath with path parameter', () async {
6164
const String pattern = '/user/:id/book/:bookId';
6265
final List<String> pathParameter = <String>[];
63-
final RegExp regex = patternToRegExp(pattern, pathParameter);
66+
final RegExp regex =
67+
patternToRegExp(pattern, pathParameter, caseSensitive: true);
6468

6569
const String url = '/user/123/book/456';
6670
final RegExpMatch? match = regex.firstMatch(url);

packages/go_router/test/route_data_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class _ShellRouteDataWithKey extends ShellRouteData {
5050
GoRouterState state,
5151
Widget navigator,
5252
) =>
53-
SizedBox(
53+
KeyedSubtree(
5454
key: key,
5555
child: navigator,
5656
);

0 commit comments

Comments
 (0)