Skip to content

Commit 9c3a689

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 9c3a689

File tree

3 files changed

+54
-19
lines changed

3 files changed

+54
-19
lines changed

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,24 +96,26 @@ export async function renderAngular(
9696
applicationRef = await bootstrap({ platformRef });
9797
}
9898

99+
const envInjector = applicationRef.injector;
100+
const router = envInjector.get(Router);
101+
const initialUrl = router.currentNavigation()?.initialUrl.toString();
102+
99103
// Block until application is stable.
100104
await applicationRef.whenStable();
101105

102106
// TODO(alanagius): Find a way to avoid rendering here especially for redirects as any output will be discarded.
103-
const envInjector = applicationRef.injector;
104107
const routerIsProvided = !!envInjector.get(ActivatedRoute, null);
105-
const router = envInjector.get(Router);
106108
const lastSuccessfulNavigation = router.lastSuccessfulNavigation();
107109

108110
if (!routerIsProvided) {
109111
hasNavigationError = false;
110-
} else if (lastSuccessfulNavigation?.finalUrl) {
112+
} else if (lastSuccessfulNavigation?.finalUrl && initialUrl !== null) {
111113
hasNavigationError = false;
112114

113-
const { finalUrl, initialUrl } = lastSuccessfulNavigation;
115+
const { finalUrl } = lastSuccessfulNavigation;
114116
const finalUrlStringified = finalUrl.toString();
115117

116-
if (initialUrl.toString() !== finalUrlStringified) {
118+
if (initialUrl !== finalUrlStringified) {
117119
const baseHref =
118120
envInjector.get(APP_BASE_HREF, null, { optional: true }) ??
119121
envInjector.get(PlatformLocation).getBaseHrefFromDOM();

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

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,6 @@ import { RenderMode } from '../src/routes/route-config';
1919
import { setAngularAppTestingManifest } from './testing-utils';
2020

2121
function createEntryPoint(locale: string) {
22-
@Component({
23-
selector: `app-ssr-${locale}`,
24-
template: `SSR works ${locale.toUpperCase()}`,
25-
})
26-
class SSRComponent {}
27-
28-
@Component({
29-
selector: `app-ssg-${locale}`,
30-
template: `SSG works ${locale.toUpperCase()}`,
31-
})
32-
class SSGComponent {}
33-
3422
return async () => {
3523
@Component({
3624
selector: `app-home-${locale}`,

packages/angular/ssr/test/app_spec.ts

Lines changed: 47 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, inject } from '@angular/core';
15+
import { CanActivateFn, 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,46 @@ describe('AngularServerApp', () => {
2627
})
2728
class HomeComponent {}
2829

30+
@Component({
31+
selector: 'app-redirect',
32+
})
33+
class RedirectComponent {
34+
constructor() {
35+
void inject(Router).navigate([], {
36+
queryParams: { filter: 'test' },
37+
});
38+
}
39+
}
40+
41+
const queryParamAdderGuard: CanActivateFn = (_route, state) => {
42+
const urlTree = inject(Router).parseUrl(state.url);
43+
44+
if (urlTree.queryParamMap.has('filter')) {
45+
return true;
46+
}
47+
48+
urlTree.queryParams = {
49+
filter: 'test',
50+
};
51+
52+
return urlTree;
53+
};
54+
2955
setAngularAppTestingManifest(
3056
[
3157
{ path: 'home', component: HomeComponent },
3258
{ path: 'home-csr', component: HomeComponent },
3359
{ path: 'home-ssg', component: HomeComponent },
3460
{ path: 'page-with-headers', component: HomeComponent },
3561
{ path: 'page-with-status', component: HomeComponent },
62+
3663
{ path: 'redirect', redirectTo: 'home' },
64+
{ path: 'redirect-via-navigate', component: RedirectComponent },
65+
{
66+
path: 'redirect-via-guard',
67+
canActivate: [queryParamAdderGuard],
68+
component: HomeComponent,
69+
},
3770
{ path: 'redirect/relative', redirectTo: 'home' },
3871
{ path: 'redirect/:param/relative', redirectTo: 'home' },
3972
{ path: 'redirect/absolute', redirectTo: '/home' },
@@ -259,11 +292,23 @@ describe('AngularServerApp', () => {
259292
});
260293

261294
describe('SSR pages', () => {
262-
it('returns a 302 status and redirects to the correct location when redirectTo is a function', async () => {
295+
it('returns a 302 status and redirects to the correct location when `redirectTo` is a function', async () => {
263296
const response = await app.handle(new Request('http://localhost/redirect-to-function'));
264297
expect(response?.headers.get('location')).toBe('/home');
265298
expect(response?.status).toBe(302);
266299
});
300+
301+
it('returns a 302 status and redirects to the correct location when `router.navigate` is used', async () => {
302+
const response = await app.handle(new Request('http://localhost/redirect-via-navigate'));
303+
expect(response?.headers.get('location')).toBe('/redirect-via-navigate?filter=test');
304+
expect(response?.status).toBe(302);
305+
});
306+
307+
it('returns a 302 status and redirects to the correct location when `urlTree` is updated in a guard', async () => {
308+
const response = await app.handle(new Request('http://localhost/redirect-via-guard'));
309+
expect(response?.headers.get('location')).toBe('/redirect-via-guard?filter=test');
310+
expect(response?.status).toBe(302);
311+
});
267312
});
268313
});
269314
});

0 commit comments

Comments
 (0)