Skip to content

Commit 1abe68a

Browse files
alan-agius4dgp1130
authored andcommitted
fix(@angular/ssr): prevent redirect loop with encoded query parameters
Previously, encoded query parameters caused a mismatch between the requested URL and the reconstructed URL, leading to a redirect loop. This change ensures both URLs are decoded before comparison. Closes #31881 (cherry picked from commit 61a027d)
1 parent 4dfc314 commit 1abe68a

File tree

3 files changed

+55
-4
lines changed

3 files changed

+55
-4
lines changed

packages/angular/ssr/src/utils/ng.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,15 @@ export async function renderAngular(
107107

108108
if (!routerIsProvided) {
109109
hasNavigationError = false;
110-
} else if (lastSuccessfulNavigation) {
110+
} else if (lastSuccessfulNavigation?.finalUrl) {
111111
hasNavigationError = false;
112+
112113
const { pathname, search, hash } = envInjector.get(PlatformLocation);
113-
const finalUrl = [stripTrailingSlash(pathname), search, hash].join('');
114+
const finalUrl = constructDecodedUrl({ pathname, search, hash });
115+
const urlToRenderString = constructDecodedUrl(urlToRender);
114116

115-
if (urlToRender.href !== new URL(finalUrl, urlToRender.origin).href) {
116-
redirectTo = finalUrl;
117+
if (urlToRenderString !== finalUrl) {
118+
redirectTo = [pathname, search, hash].join('');
117119
}
118120
}
119121

@@ -171,3 +173,23 @@ function asyncDestroyPlatform(platformRef: PlatformRef): Promise<void> {
171173
}, 0);
172174
});
173175
}
176+
177+
/**
178+
* Constructs a decoded URL string from its components, ensuring consistency for comparison.
179+
*
180+
* This function takes a URL-like object (containing `pathname`, `search`, and `hash`),
181+
* strips the trailing slash from the pathname, joins the components, and then decodes
182+
* the entire string. This normalization is crucial for accurately comparing URLs
183+
* that might differ only in encoding or trailing slashes.
184+
*
185+
* @param url - An object containing the URL components:
186+
* - `pathname`: The path of the URL.
187+
* - `search`: The query string of the URL (including '?').
188+
* - `hash`: The hash fragment of the URL (including '#').
189+
* @returns The constructed and decoded URL string.
190+
*/
191+
function constructDecodedUrl(url: { pathname: string; search: string; hash: string }): string {
192+
const joinedUrl = [stripTrailingSlash(url.pathname), url.search, url.hash].join('');
193+
194+
return decodeURIComponent(joinedUrl);
195+
}

packages/angular/ssr/src/utils/url.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,18 @@ export function stripMatrixParams(pathname: string): string {
220220
// This regex finds all occurrences of a semicolon followed by any characters
221221
return pathname.includes(';') ? pathname.replace(MATRIX_PARAMS_REGEX, '') : pathname;
222222
}
223+
224+
/**
225+
* Constructs a decoded URL string from its components.
226+
*
227+
* This function joins the pathname (with trailing slash removed), search, and hash,
228+
* and then decodes the result.
229+
*
230+
* @param pathname - The path of the URL.
231+
* @param search - The query string of the URL (including '?').
232+
* @param hash - The hash fragment of the URL (including '#').
233+
* @returns The constructed and decoded URL string.
234+
*/
235+
export function constructUrl(pathname: string, search: string, hash: string): string {
236+
return decodeURIComponent([stripTrailingSlash(pathname), search, hash].join(''));
237+
}

packages/angular/ssr/test/app-engine_spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,5 +273,19 @@ describe('AngularAppEngine', () => {
273273
const response = await appEngine.handle(request);
274274
expect(await response?.text()).toContain('Home works');
275275
});
276+
277+
it('should work with encoded characters', async () => {
278+
const request = new Request('https://example.com/home?email=xyz%40xyz.com');
279+
const response = await appEngine.handle(request);
280+
expect(response?.status).toBe(200);
281+
expect(await response?.text()).toContain('Home works');
282+
});
283+
284+
it('should work with decoded characters', async () => {
285+
const request = new Request('https://example.com/[email protected]');
286+
const response = await appEngine.handle(request);
287+
expect(response?.status).toBe(200);
288+
expect(await response?.text()).toContain('Home works');
289+
});
276290
});
277291
});

0 commit comments

Comments
 (0)