Skip to content

Commit cf9f7db

Browse files
committed
fix(@angular/ssr): ensure server-side navigation triggers a redirect
When a navigation occurs on the server-side, such as using `router.navigate`, and the final URL is different from the initial URL that was requested, the server should respond with a redirect. Previously, the initial URL was being read from `router.lastSuccessfulNavigation.initialUrl`, which could be incorrect in scenarios involving server-side navigations, causing the comparison with the final URL to fail and preventing the redirect. This change ensures that the initial URL requested by the browser is used for the comparison, correctly triggering a redirect when necessary. Closes #31482
1 parent 53180a8 commit cf9f7db

File tree

2 files changed

+37
-12
lines changed

2 files changed

+37
-12
lines changed

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ export async function renderAngular(
6060
serverContext: string,
6161
): Promise<{ hasNavigationError: boolean; redirectTo?: string; content: () => Promise<string> }> {
6262
// A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`.
63-
const urlToRender = stripIndexHtmlFromURL(url).toString();
63+
const urlToRender = stripIndexHtmlFromURL(url);
6464
const platformRef = platformServer([
6565
{
6666
provide: INITIAL_CONFIG,
6767
useValue: {
68-
url: urlToRender,
68+
url: urlToRender.toString(),
6969
document: html,
7070
},
7171
},
@@ -110,15 +110,15 @@ export async function renderAngular(
110110
} else if (lastSuccessfulNavigation?.finalUrl) {
111111
hasNavigationError = false;
112112

113-
const { finalUrl, initialUrl } = lastSuccessfulNavigation;
114-
const finalUrlStringified = finalUrl.toString();
113+
const baseHref =
114+
envInjector.get(APP_BASE_HREF, null, { optional: true }) ??
115+
envInjector.get(PlatformLocation).getBaseHrefFromDOM();
116+
const finalUrlStringified = lastSuccessfulNavigation.finalUrl.toString();
117+
const finalUrlWithBaseHref = joinUrlParts(baseHref, finalUrlStringified);
118+
const initialUrl = `${urlToRender.pathname}${urlToRender.search}${urlToRender.hash}`;
115119

116-
if (initialUrl.toString() !== finalUrlStringified) {
117-
const baseHref =
118-
envInjector.get(APP_BASE_HREF, null, { optional: true }) ??
119-
envInjector.get(PlatformLocation).getBaseHrefFromDOM();
120-
121-
redirectTo = joinUrlParts(baseHref, finalUrlStringified);
120+
if (initialUrl !== finalUrlWithBaseHref) {
121+
redirectTo = finalUrlWithBaseHref;
122122
}
123123
}
124124

packages/angular/ssr/test/app_spec.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
import '@angular/compiler';
1212
/* eslint-enable import/no-unassigned-import */
1313

14-
import { Component } from '@angular/core';
14+
import { Component, PendingTasks, inject } from '@angular/core';
15+
import { Router } from '@angular/router';
1516
import { AngularServerApp } from '../src/app';
1617
import { RenderMode } from '../src/routes/route-config';
1718
import { setAngularAppTestingManifest } from './testing-utils';
@@ -26,14 +27,32 @@ describe('AngularServerApp', () => {
2627
})
2728
class HomeComponent {}
2829

30+
@Component({
31+
selector: 'app-redirect',
32+
})
33+
class RedirectComponent {
34+
constructor() {
35+
const router = inject(Router);
36+
const pendingTasks = inject(PendingTasks);
37+
38+
pendingTasks.run(() =>
39+
router.navigate([], {
40+
queryParams: { filter: 'test' },
41+
}),
42+
);
43+
}
44+
}
45+
2946
setAngularAppTestingManifest(
3047
[
3148
{ path: 'home', component: HomeComponent },
3249
{ path: 'home-csr', component: HomeComponent },
3350
{ path: 'home-ssg', component: HomeComponent },
3451
{ path: 'page-with-headers', component: HomeComponent },
3552
{ path: 'page-with-status', component: HomeComponent },
53+
3654
{ path: 'redirect', redirectTo: 'home' },
55+
{ path: 'redirect-via-navigate', component: RedirectComponent },
3756
{ path: 'redirect/relative', redirectTo: 'home' },
3857
{ path: 'redirect/:param/relative', redirectTo: 'home' },
3958
{ path: 'redirect/absolute', redirectTo: '/home' },
@@ -259,11 +278,17 @@ describe('AngularServerApp', () => {
259278
});
260279

261280
describe('SSR pages', () => {
262-
it('returns a 302 status and redirects to the correct location when redirectTo is a function', async () => {
281+
it('returns a 302 status and redirects to the correct location when `redirectTo` is a function', async () => {
263282
const response = await app.handle(new Request('http://localhost/redirect-to-function'));
264283
expect(response?.headers.get('location')).toBe('/home');
265284
expect(response?.status).toBe(302);
266285
});
286+
287+
it('returns a 302 status and redirects to the correct location when `router.navigate` is used', async () => {
288+
const response = await app.handle(new Request('http://localhost/redirect-via-navigate'));
289+
expect(response?.headers.get('location')).toBe('/redirect-via-navigate?filter=test');
290+
expect(response?.status).toBe(302);
291+
});
267292
});
268293
});
269294
});

0 commit comments

Comments
 (0)