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
20 changes: 2 additions & 18 deletions packages/angular/ssr/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { InlineCriticalCssProcessor } from './utils/inline-critical-css';
import { LRUCache } from './utils/lru-cache';
import { AngularBootstrap, renderAngular } from './utils/ng';
import { promiseWithAbort } from './utils/promise';
import { createRedirectResponse } from './utils/redirect';
import { buildPathWithParams, joinUrlParts, stripLeadingSlash } from './utils/url';

/**
Expand Down Expand Up @@ -351,7 +352,7 @@ export class AngularServerApp {
}

if (result.redirectTo) {
return createRedirectResponse(result.redirectTo, status);
return createRedirectResponse(result.redirectTo, responseInit.status);
}

if (renderMode === RenderMode.Prerender) {
Expand Down Expand Up @@ -546,20 +547,3 @@ function appendPreloadHintsToHtml(html: string, preload: readonly string[]): str
html.slice(bodyCloseIdx),
].join('\n');
}

/**
* Creates an HTTP redirect response with a specified location and status code.
*
* @param location - The URL to which the response should redirect.
* @param status - The HTTP status code for the redirection. Defaults to 302 (Found).
* See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
* @returns A `Response` object representing the HTTP redirect.
*/
function createRedirectResponse(location: string, status = 302): Response {
return new Response(null, {
status,
headers: {
'Location': location,
},
});
}
8 changes: 2 additions & 6 deletions packages/angular/ssr/src/routes/ng-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { Console } from '../console';
import { AngularAppManifest, getAngularAppManifest } from '../manifest';
import { AngularBootstrap, isNgModule } from '../utils/ng';
import { promiseWithAbort } from '../utils/promise';
import { VALID_REDIRECT_RESPONSE_CODES, isValidRedirectResponseCode } from '../utils/redirect';
import { addTrailingSlash, joinUrlParts, stripLeadingSlash } from '../utils/url';
import {
PrerenderFallback,
Expand Down Expand Up @@ -59,11 +60,6 @@ const CATCH_ALL_REGEXP = /\/(\*\*)$/;
*/
const URL_PARAMETER_REGEXP = /(?<!\\):([^/]+)/g;

/**
* An set of HTTP status codes that are considered valid for redirect responses.
*/
const VALID_REDIRECT_RESPONSE_CODES = new Set([301, 302, 303, 307, 308]);

/**
* Additional metadata for a server configuration route tree.
*/
Expand Down Expand Up @@ -163,7 +159,7 @@ async function* handleRoute(options: {
includePrerenderFallbackRoutes,
);
} else if (redirectTo !== undefined) {
if (metadata.status && !VALID_REDIRECT_RESPONSE_CODES.has(metadata.status)) {
if (metadata.status && !isValidRedirectResponseCode(metadata.status)) {
yield {
error:
`The '${metadata.status}' status code is not a valid redirect response code. ` +
Expand Down
46 changes: 46 additions & 0 deletions packages/angular/ssr/src/utils/redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

/**
* An set of HTTP status codes that are considered valid for redirect responses.
*/
export const VALID_REDIRECT_RESPONSE_CODES = new Set([301, 302, 303, 307, 308]);

/**
* Checks if the given HTTP status code is a valid redirect response code.
*
* @param code The HTTP status code to check.
* @returns `true` if the code is a valid redirect response code, `false` otherwise.
*/
export function isValidRedirectResponseCode(code: number): boolean {
return VALID_REDIRECT_RESPONSE_CODES.has(code);
}

/**
* Creates an HTTP redirect response with a specified location and status code.
*
* @param location - The URL to which the response should redirect.
* @param status - The HTTP status code for the redirection. Defaults to 302 (Found).
* See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
* @returns A `Response` object representing the HTTP redirect.
*/
export function createRedirectResponse(location: string, status = 302): Response {
if (ngDevMode && !isValidRedirectResponseCode(status)) {
throw new Error(
`Invalid redirect status code: ${status}. ` +
`Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`,
);
}

return new Response(null, {
status,
headers: {
'Location': location,
},
});
}
10 changes: 8 additions & 2 deletions packages/angular/ssr/test/app_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import '@angular/compiler';
/* eslint-enable import/no-unassigned-import */

import { APP_BASE_HREF } from '@angular/common';
import { Component, REQUEST, inject } from '@angular/core';
import { Component, REQUEST, RESPONSE_INIT, inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AngularServerApp } from '../src/app';
import { RenderMode } from '../src/routes/route-config';
Expand All @@ -33,6 +33,12 @@ describe('AngularServerApp', () => {
})
class RedirectComponent {
constructor() {
const responseInit = inject(RESPONSE_INIT);
if (responseInit) {
// TODO(alanagius): Remove once https://github.com/angular/angular/pull/66126 is merged and released.
(responseInit as { status: number }).status = 308;
}

void inject(Router).navigate([], {
queryParams: { filter: 'test' },
});
Expand Down Expand Up @@ -310,7 +316,7 @@ describe('AngularServerApp', () => {
it('returns a 302 status and redirects to the correct location when `router.navigate` is used', async () => {
const response = await app.handle(new Request('http://localhost/redirect-via-navigate'));
expect(response?.headers.get('location')).toBe('/redirect-via-navigate?filter=test');
expect(response?.status).toBe(302);
expect(response?.status).toBe(308);
});

it('returns a 302 status and redirects to the correct location when `urlTree` is updated in a guard', async () => {
Expand Down