diff --git a/packages/angular/ssr/src/utils/ng.ts b/packages/angular/ssr/src/utils/ng.ts index 120fdf940dd6..5d10560db39d 100644 --- a/packages/angular/ssr/src/utils/ng.ts +++ b/packages/angular/ssr/src/utils/ng.ts @@ -107,13 +107,15 @@ export async function renderAngular( if (!routerIsProvided) { hasNavigationError = false; - } else if (lastSuccessfulNavigation) { + } else if (lastSuccessfulNavigation?.finalUrl) { hasNavigationError = false; + const { pathname, search, hash } = envInjector.get(PlatformLocation); - const finalUrl = [stripTrailingSlash(pathname), search, hash].join(''); + const finalUrl = constructDecodedUrl({ pathname, search, hash }); + const urlToRenderString = constructDecodedUrl(urlToRender); - if (urlToRender.href !== new URL(finalUrl, urlToRender.origin).href) { - redirectTo = finalUrl; + if (urlToRenderString !== finalUrl) { + redirectTo = [pathname, search, hash].join(''); } } @@ -171,3 +173,23 @@ function asyncDestroyPlatform(platformRef: PlatformRef): Promise { }, 0); }); } + +/** + * Constructs a decoded URL string from its components, ensuring consistency for comparison. + * + * This function takes a URL-like object (containing `pathname`, `search`, and `hash`), + * strips the trailing slash from the pathname, joins the components, and then decodes + * the entire string. This normalization is crucial for accurately comparing URLs + * that might differ only in encoding or trailing slashes. + * + * @param url - An object containing the URL components: + * - `pathname`: The path of the URL. + * - `search`: The query string of the URL (including '?'). + * - `hash`: The hash fragment of the URL (including '#'). + * @returns The constructed and decoded URL string. + */ +function constructDecodedUrl(url: { pathname: string; search: string; hash: string }): string { + const joinedUrl = [stripTrailingSlash(url.pathname), url.search, url.hash].join(''); + + return decodeURIComponent(joinedUrl); +} diff --git a/packages/angular/ssr/src/utils/url.ts b/packages/angular/ssr/src/utils/url.ts index 55d37ad9a05f..1fa756e19c19 100644 --- a/packages/angular/ssr/src/utils/url.ts +++ b/packages/angular/ssr/src/utils/url.ts @@ -220,3 +220,18 @@ export function stripMatrixParams(pathname: string): string { // This regex finds all occurrences of a semicolon followed by any characters return pathname.includes(';') ? pathname.replace(MATRIX_PARAMS_REGEX, '') : pathname; } + +/** + * Constructs a decoded URL string from its components. + * + * This function joins the pathname (with trailing slash removed), search, and hash, + * and then decodes the result. + * + * @param pathname - The path of the URL. + * @param search - The query string of the URL (including '?'). + * @param hash - The hash fragment of the URL (including '#'). + * @returns The constructed and decoded URL string. + */ +export function constructUrl(pathname: string, search: string, hash: string): string { + return decodeURIComponent([stripTrailingSlash(pathname), search, hash].join('')); +} diff --git a/packages/angular/ssr/test/app-engine_spec.ts b/packages/angular/ssr/test/app-engine_spec.ts index b08931b9400b..bfc824b6d260 100644 --- a/packages/angular/ssr/test/app-engine_spec.ts +++ b/packages/angular/ssr/test/app-engine_spec.ts @@ -269,5 +269,19 @@ describe('AngularAppEngine', () => { const response = await appEngine.handle(request); expect(await response?.text()).toContain('Home works'); }); + + it('should work with encoded characters', async () => { + const request = new Request('https://example.com/home?email=xyz%40xyz.com'); + const response = await appEngine.handle(request); + expect(response?.status).toBe(200); + expect(await response?.text()).toContain('Home works'); + }); + + it('should work with decoded characters', async () => { + const request = new Request('https://example.com/home?email=xyz@xyz.com'); + const response = await appEngine.handle(request); + expect(response?.status).toBe(200); + expect(await response?.text()).toContain('Home works'); + }); }); });