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 ; }