Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion packages/angular/ssr/src/routes/route-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
* @returns An array of path segments.
*/
private getPathSegments(route: string): string[] {
return route.split('/').filter(Boolean);
return route.split('/').filter(Boolean).map(decodeURIComponent);
}

/**
Expand Down
1 change: 0 additions & 1 deletion packages/angular/ssr/src/routes/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
27 changes: 17 additions & 10 deletions packages/angular/ssr/src/utils/ng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
alan-agius4 marked this conversation as resolved.

if (urlToRenderString !== finalUrl) {
redirectTo = [pathname, search, hash].join('');
Expand Down Expand Up @@ -190,22 +190,27 @@ function asyncDestroyPlatform(platformRef: PlatformRef): Promise<void> {
}

/**
* 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,
Comment thread
alan-agius4 marked this conversation as resolved.
url: { pathname: string; search: string; hash: string },
prefix?: string | null,
): string {
Expand All @@ -219,5 +224,7 @@ function constructDecodedUrl(

urlParts.push(search, hash);

return decodeURIComponent(urlParts.join(''));
const urlTree = router.parseUrl(urlParts.join(''));

return router.serializeUrl(urlTree);
}
22 changes: 17 additions & 5 deletions packages/angular/ssr/test/app_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/angular/ssr/test/routes/router_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
});
});
Loading