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
30 changes: 26 additions & 4 deletions packages/angular/ssr/src/utils/ng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
}
}

Expand Down Expand Up @@ -171,3 +173,23 @@ function asyncDestroyPlatform(platformRef: PlatformRef): Promise<void> {
}, 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);
}
15 changes: 15 additions & 0 deletions packages/angular/ssr/src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(''));
}
14 changes: 14 additions & 0 deletions packages/angular/ssr/test/app-engine_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});