diff --git a/plugins/app/src/extensions/AppRoutes.test.tsx b/plugins/app/src/extensions/AppRoutes.test.tsx
index f8ce09a155..83b0c596b4 100644
--- a/plugins/app/src/extensions/AppRoutes.test.tsx
+++ b/plugins/app/src/extensions/AppRoutes.test.tsx
@@ -539,6 +539,53 @@ describe('AppRoutes', () => {
});
});
+ it('should not corrupt a longer param when a shorter param is a prefix of it', async () => {
+ const LocationDisplay = () => {
+ const location = useLocation();
+ return
{location.pathname}
;
+ };
+
+ const targetPage = PageBlueprint.make({
+ name: 'target',
+ params: {
+ path: '/target/:ab/:a',
+ loader: async () => (
+
+ Target Page
+
+
+ ),
+ },
+ });
+
+ renderTestApp({
+ extensions: [targetPage],
+ initialRouteEntries: ['/source/bar/foo'],
+ config: {
+ ...DEFAULT_CONFIG,
+ app: {
+ ...DEFAULT_CONFIG.app,
+ extensions: [
+ {
+ 'app/routes': {
+ config: {
+ redirects: [{ from: '/source/:ab/:a', to: '/target/:ab/:a' }],
+ },
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText('Target Page')).toBeInTheDocument();
+ expect(screen.getByTestId('location')).toHaveTextContent(
+ '/target/bar/foo',
+ );
+ });
+ });
+
it('should not interfere with normal routes when redirects are configured', async () => {
const homePage = PageBlueprint.make({
name: 'home',
diff --git a/plugins/app/src/extensions/AppRoutes.tsx b/plugins/app/src/extensions/AppRoutes.tsx
index 8dd05aaa5a..04d94ebc35 100644
--- a/plugins/app/src/extensions/AppRoutes.tsx
+++ b/plugins/app/src/extensions/AppRoutes.tsx
@@ -27,7 +27,12 @@ function RedirectWithParams({ to }: { to: string }) {
const params = useParams() as Record;
let target = to;
for (const [name, value] of Object.entries(params)) {
- target = target.replaceAll(name === '*' ? '*' : `:${name}`, value ?? '');
+ // Use \b (word boundary) for named params so that `:a` doesn't
+ // accidentally match inside `:ab` when both are present.
+ target = target.replace(
+ name === '*' ? /\*/g : new RegExp(`:${name}\\b`, 'g'),
+ value ?? '',
+ );
}
return ;
}