diff --git a/packages/angular/ssr/src/routes/route-tree.ts b/packages/angular/ssr/src/routes/route-tree.ts index 68287de7f521..ff689a3c90f5 100644 --- a/packages/angular/ssr/src/routes/route-tree.ts +++ b/packages/angular/ssr/src/routes/route-tree.ts @@ -207,7 +207,7 @@ export class RouteTree = {}> * @returns An array of path segments. */ private getPathSegments(route: string): string[] { - return route.split('/').filter(Boolean); + return route.split('/').filter(Boolean).map(decodeURIComponent); } /** diff --git a/packages/angular/ssr/src/routes/router.ts b/packages/angular/ssr/src/routes/router.ts index f01e9989028e..16b6a83c90f1 100644 --- a/packages/angular/ssr/src/routes/router.ts +++ b/packages/angular/ssr/src/routes/router.ts @@ -87,7 +87,6 @@ export class ServerRouter { // A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`. let { pathname } = stripIndexHtmlFromURL(url); pathname = stripMatrixParams(pathname); - pathname = decodeURIComponent(pathname); return this.routeTree.match(pathname); } diff --git a/packages/angular/ssr/src/utils/ng.ts b/packages/angular/ssr/src/utils/ng.ts index acf2df180f32..2c4ada0b8c6d 100644 --- a/packages/angular/ssr/src/utils/ng.ts +++ b/packages/angular/ssr/src/utils/ng.ts @@ -126,8 +126,8 @@ export async function renderAngular( envInjector.get(REQUEST, null, { optional: true })?.headers.get('X-Forwarded-Prefix'); const { pathname, search, hash } = envInjector.get(PlatformLocation); - const finalUrl = constructDecodedUrl({ pathname, search, hash }, requestPrefix); - const urlToRenderString = constructDecodedUrl(urlToRender, requestPrefix); + const finalUrl = constructSerializedUrl(router, { pathname, search, hash }, requestPrefix); + const urlToRenderString = constructSerializedUrl(router, urlToRender, requestPrefix); if (urlToRenderString !== finalUrl) { redirectTo = [pathname, search, hash].join(''); @@ -190,22 +190,27 @@ function asyncDestroyPlatform(platformRef: PlatformRef): Promise { } /** - * Constructs a decoded URL string from its components, ensuring consistency for comparison. + * Constructs a normalized and serialized URL string from its components. * - * 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. + * This function uses the provided `Router` instance to parse and serialize the URL, + * ensuring that the resulting string is consistent with the router's configuration. + * It also handles the optional `prefix` parameter to ensure proper URL construction. * + * @param router - The `Router` instance to use for parsing and serializing the URL. * @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 '#'). * @param prefix - An optional prefix (e.g., `APP_BASE_HREF`) to prepend to the pathname * if it is not already present. - * @returns The constructed and decoded URL string. + * @returns The normalized and serialized URL string. + * + * @note + * We use the Angular `Router` to construct the URL, so that the URL is consistent with the router's configuration. + * This is important for the URL to be correctly parsed and serialized by the router as it might have different encodings. */ -function constructDecodedUrl( +function constructSerializedUrl( + router: Router, url: { pathname: string; search: string; hash: string }, prefix?: string | null, ): string { @@ -219,5 +224,7 @@ function constructDecodedUrl( urlParts.push(search, hash); - return decodeURIComponent(urlParts.join('')); + const urlTree = router.parseUrl(urlParts.join('')); + + return router.serializeUrl(urlTree); } diff --git a/packages/angular/ssr/test/app_spec.ts b/packages/angular/ssr/test/app_spec.ts index 8fb82a10cfb9..b35424796467 100644 --- a/packages/angular/ssr/test/app_spec.ts +++ b/packages/angular/ssr/test/app_spec.ts @@ -334,11 +334,23 @@ describe('AngularServerApp', () => { expect(response?.status).toBe(302); }); - it('should work with encoded characters', async () => { - const request = new Request('http://localhost/home?email=xyz%40xyz.com'); - const response = await app.handle(request); - expect(response?.status).toBe(200); - expect(await response?.text()).toContain('Home works'); + it('should work with complex and encoded URLs', async () => { + const urls = [ + 'http://localhost/home?email=xyz%40xyz.com', + 'http://localhost/home?empty', + 'http://localhost/home?scope=email+profile', + 'http://localhost/home?bbb=1&aaa=2&bbb=3', + 'http://localhost//home', + ]; + + for (const url of urls) { + const request = new Request(url); + const response = await app.handle(request); + expect(response?.status).withContext(`url: ${url}`).toBe(200); + expect(await response?.text()) + .withContext(`url: ${url}`) + .toContain('Home works'); + } }); it('should work with decoded characters', async () => { diff --git a/packages/angular/ssr/test/routes/router_spec.ts b/packages/angular/ssr/test/routes/router_spec.ts index 1ce317f414c7..380bc659549a 100644 --- a/packages/angular/ssr/test/routes/router_spec.ts +++ b/packages/angular/ssr/test/routes/router_spec.ts @@ -127,5 +127,15 @@ describe('ServerRouter', () => { renderMode: RenderMode.Server, }); }); + + it('should handle encoded params', () => { + const encodedUserMetadata = router.match( + new URL('http://localhost/user/Bob%20%2F%20Roberts'), + ); + expect(encodedUserMetadata).toEqual({ + route: '/user/*', + renderMode: RenderMode.Server, + }); + }); }); });