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
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.
23 changes: 15 additions & 8 deletions packages/go_router/lib/src/configuration.dart
Original file line number Diff line number Diff line change
@@ -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].
@@ -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(
@@ -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');
@@ -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,
@@ -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)'}');
}
}

@@ -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) {
13 changes: 12 additions & 1 deletion packages/go_router/lib/src/match.dart
Original file line number Diff line number Diff line change
@@ -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(
5 changes: 3 additions & 2 deletions packages/go_router/lib/src/path_utils.dart
Original file line number Diff line number Diff line change
@@ -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)) {
@@ -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]) {
6 changes: 4 additions & 2 deletions packages/go_router/lib/src/route.dart
Original file line number Diff line number Diff line change
@@ -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.
@@ -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(
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

54 changes: 39 additions & 15 deletions packages/go_router/test/go_router_test.dart
Original file line number Diff line number Diff line change
@@ -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';
@@ -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: '/',
@@ -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);
@@ -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(
12 changes: 8 additions & 4 deletions packages/go_router/test/path_utils_test.dart
Original file line number Diff line number Diff line change
@@ -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);
@@ -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');
@@ -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);
@@ -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);
2 changes: 1 addition & 1 deletion packages/go_router/test/route_data_test.dart
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ class _ShellRouteDataWithKey extends ShellRouteData {
GoRouterState state,
Widget navigator,
) =>
SizedBox(
KeyedSubtree(
key: key,
child: navigator,
);