Skip to content

[go_router] Fix routing to treat URLs with different cases (e.g., /Home vs /home) as distinct routes. #9426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 15.2.4

- Fixes routing to treat URLs with different cases (e.g., `/Home` vs `/home`) as distinct routes.

## 15.2.3

- Updates Type-safe routes topic documentation to use the mixin from `go_router_builder` 3.0.0.
Expand Down
23 changes: 15 additions & 8 deletions packages/go_router/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import 'state.dart';
typedef GoRouterRedirect = FutureOr<String?> Function(
BuildContext context, GoRouterState state);

typedef _NamedPath = ({String path, bool caseSensitive});

/// The route configuration for GoRouter configured by the app.
class RouteConfiguration {
/// Constructs a [RouteConfiguration].
Expand Down Expand Up @@ -246,7 +248,7 @@ class RouteConfiguration {
/// example.
final Codec<Object?, Object?>? extraCodec;

final Map<String, String> _nameToPath = <String, String>{};
final Map<String, _NamedPath> _nameToPath = <String, _NamedPath>{};

/// Looks up the url location by a [GoRoute]'s name.
String namedLocation(
Expand All @@ -264,11 +266,11 @@ class RouteConfiguration {
return true;
}());
assert(_nameToPath.containsKey(name), 'unknown route name: $name');
final String path = _nameToPath[name]!;
final _NamedPath path = _nameToPath[name]!;
assert(() {
// Check that all required params are present
final List<String> paramNames = <String>[];
patternToRegExp(path, paramNames);
patternToRegExp(path.path, paramNames, caseSensitive: path.caseSensitive);
for (final String paramName in paramNames) {
assert(pathParameters.containsKey(paramName),
'missing param "$paramName" for $path');
Expand All @@ -284,7 +286,10 @@ class RouteConfiguration {
for (final MapEntry<String, String> param in pathParameters.entries)
param.key: Uri.encodeComponent(param.value)
};
final String location = patternToPath(path, encodedParams);
final String location = patternToPath(
path.path,
encodedParams,
);
return Uri(
path: location,
queryParameters: queryParameters.isEmpty ? null : queryParameters,
Expand Down Expand Up @@ -528,8 +533,9 @@ class RouteConfiguration {

if (_nameToPath.isNotEmpty) {
sb.writeln('known full paths for route names:');
for (final MapEntry<String, String> e in _nameToPath.entries) {
sb.writeln(' ${e.key} => ${e.value}');
for (final MapEntry<String, _NamedPath> e in _nameToPath.entries) {
sb.writeln(
' ${e.key} => ${e.value.path}${e.value.caseSensitive ? '' : ' (case-insensitive)'}');
}
}

Expand Down Expand Up @@ -594,8 +600,9 @@ class RouteConfiguration {
assert(
!_nameToPath.containsKey(name),
'duplication fullpaths for name '
'"$name":${_nameToPath[name]}, $fullPath');
_nameToPath[name] = fullPath;
'"$name":${_nameToPath[name]!.path}, $fullPath');
_nameToPath[name] =
(path: fullPath, caseSensitive: route.caseSensitive);
}

if (route.routes.isNotEmpty) {
Expand Down
13 changes: 12 additions & 1 deletion packages/go_router/lib/src/match.dart
Original file line number Diff line number Diff line change
Expand Up @@ -660,9 +660,20 @@ class RouteMatchList with Diagnosticable {
matches: newMatches,
);
}

if (newMatches.isEmpty) {
return RouteMatchList.empty;
}

RouteBase newRoute = newMatches.last.route;
while (newRoute is ShellRouteBase) {
newRoute = newRoute.routes.last;
}
newRoute as GoRoute;
// Need to remove path parameters that are no longer in the fullPath.
final List<String> newParameters = <String>[];
patternToRegExp(fullPath, newParameters);
patternToRegExp(fullPath, newParameters,
caseSensitive: newRoute.caseSensitive);
final Set<String> validParameters = newParameters.toSet();
final Map<String, String> newPathParameters =
Map<String, String>.fromEntries(
Expand Down
5 changes: 3 additions & 2 deletions packages/go_router/lib/src/path_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?');
/// To extract the path parameter values from a [RegExpMatch], pass the
/// [RegExpMatch] into [extractPathParameters] with the `parameters` that are
/// used for generating the [RegExp].
RegExp patternToRegExp(String pattern, List<String> parameters) {
RegExp patternToRegExp(String pattern, List<String> parameters,
{required bool caseSensitive}) {
final StringBuffer buffer = StringBuffer('^');
int start = 0;
for (final RegExpMatch match in _parameterRegExp.allMatches(pattern)) {
Expand All @@ -47,7 +48,7 @@ RegExp patternToRegExp(String pattern, List<String> parameters) {
if (!pattern.endsWith('/')) {
buffer.write(r'(?=/|$)');
}
return RegExp(buffer.toString(), caseSensitive: false);
return RegExp(buffer.toString(), caseSensitive: caseSensitive);
}

String _escapeGroup(String group, [String? name]) {
Expand Down
6 changes: 4 additions & 2 deletions packages/go_router/lib/src/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@ class GoRoute extends RouteBase {
'if onExit is provided, one of pageBuilder or builder must be provided'),
super._() {
// cache the path regexp and parameters
_pathRE = patternToRegExp(path, pathParameters);
_pathRE =
patternToRegExp(path, pathParameters, caseSensitive: caseSensitive);
}

/// Whether this [GoRoute] only redirects to another route.
Expand Down Expand Up @@ -1193,7 +1194,8 @@ class StatefulNavigationShell extends StatefulWidget {
/// find the first GoRoute, from which a full path will be derived.
final GoRoute route = branch.defaultRoute!;
final List<String> parameters = <String>[];
patternToRegExp(route.path, parameters);
patternToRegExp(route.path, parameters,
caseSensitive: route.caseSensitive);
assert(parameters.isEmpty);
final String fullPath = _router.configuration.locationForRoute(route)!;
return patternToPath(
Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
version: 15.2.3
version: 15.2.4
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

Expand Down
54 changes: 39 additions & 15 deletions packages/go_router/test/go_router_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/match.dart';
import 'package:go_router/src/pages/material.dart';
import 'package:logging/logging.dart';

import 'test_helpers.dart';
Expand Down Expand Up @@ -782,12 +783,6 @@ void main() {
});

testWidgets('match path case sensitively', (WidgetTester tester) async {
final FlutterExceptionHandler? oldFlutterError = FlutterError.onError;
addTearDown(() => FlutterError.onError = oldFlutterError);
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
FlutterError.onError = (FlutterErrorDetails details) {
errors.add(details);
};
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
Expand All @@ -804,16 +799,11 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
const String wrongLoc = '/FaMiLy/f2';

expect(errors, isEmpty);
router.go(wrongLoc);
await tester.pumpAndSettle();

expect(errors, hasLength(1));
expect(
errors.single.exception,
isAssertionError,
reason: 'The path is case sensitive',
);
expect(find.byType(MaterialErrorScreen), findsOne);
expect(find.text('Page Not Found'), findsOne);

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

expect(matches, hasLength(1));
expect(find.byType(FamilyScreen), findsOneWidget);
expect(errors, hasLength(1), reason: 'No new errors should be thrown');
expect(find.byType(FamilyScreen), findsOne);
});

testWidgets('supports routes with a different case',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
),
GoRoute(
path: '/abc',
builder: (BuildContext context, GoRouterState state) =>
const SizedBox(key: Key('abc')),
),
GoRoute(
path: '/ABC',
builder: (BuildContext context, GoRouterState state) =>
const SizedBox(key: Key('ABC')),
),
];

final GoRouter router = await createRouter(routes, tester);
const String loc1 = '/abc';

router.go(loc1);
await tester.pumpAndSettle();

expect(find.byKey(const Key('abc')), findsOne);

const String loc = '/ABC';
router.go(loc);
await tester.pumpAndSettle();

expect(find.byKey(const Key('ABC')), findsOne);
});

testWidgets(
Expand Down
12 changes: 8 additions & 4 deletions packages/go_router/test/path_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ void main() {
test('patternToRegExp without path parameter', () async {
const String pattern = '/settings/detail';
final List<String> pathParameter = <String>[];
final RegExp regex = patternToRegExp(pattern, pathParameter);
final RegExp regex =
patternToRegExp(pattern, pathParameter, caseSensitive: true);
expect(pathParameter.isEmpty, isTrue);
expect(regex.hasMatch('/settings/detail'), isTrue);
expect(regex.hasMatch('/settings/'), isFalse);
Expand All @@ -22,7 +23,8 @@ void main() {
test('patternToRegExp with path parameter', () async {
const String pattern = '/user/:id/book/:bookId';
final List<String> pathParameter = <String>[];
final RegExp regex = patternToRegExp(pattern, pathParameter);
final RegExp regex =
patternToRegExp(pattern, pathParameter, caseSensitive: true);
expect(pathParameter.length, 2);
expect(pathParameter[0], 'id');
expect(pathParameter[1], 'bookId');
Expand All @@ -44,7 +46,8 @@ void main() {
test('patternToPath without path parameter', () async {
const String pattern = '/settings/detail';
final List<String> pathParameter = <String>[];
final RegExp regex = patternToRegExp(pattern, pathParameter);
final RegExp regex =
patternToRegExp(pattern, pathParameter, caseSensitive: true);

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

const String url = '/user/123/book/456';
final RegExpMatch? match = regex.firstMatch(url);
Expand Down
2 changes: 1 addition & 1 deletion packages/go_router/test/route_data_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class _ShellRouteDataWithKey extends ShellRouteData {
GoRouterState state,
Widget navigator,
) =>
SizedBox(
KeyedSubtree(
key: key,
child: navigator,
);
Expand Down