Skip to content

Commit 9bb8f74

Browse files
committed
fix(@angular/ssr): support custom headers in redirect responses
Updates createRedirectResponse to accept an optional Record<string, string> of headers, allowing custom headers to be merged into the redirect response. The Location and Vary: X-Forwarded-Prefix headers are automatically set to ensure correct routing and proxy behavior. AngularServerApp now passes relevant headers from the matched route or response context when creating a redirect.
1 parent ec603ba commit 9bb8f74

File tree

3 files changed

+87
-6
lines changed

3 files changed

+87
-6
lines changed

packages/angular/ssr/src/app.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export class AngularServerApp {
190190
return null;
191191
}
192192

193-
const { redirectTo, status, renderMode } = matchedRoute;
193+
const { redirectTo, status, renderMode, headers } = matchedRoute;
194194

195195
if (redirectTo !== undefined) {
196196
return createRedirectResponse(
@@ -199,6 +199,7 @@ export class AngularServerApp {
199199
buildPathWithParams(redirectTo, url.pathname),
200200
),
201201
status,
202+
headers,
202203
);
203204
}
204205

@@ -352,7 +353,7 @@ export class AngularServerApp {
352353
}
353354

354355
if (result.redirectTo) {
355-
return createRedirectResponse(result.redirectTo, responseInit.status);
356+
return createRedirectResponse(result.redirectTo, responseInit.status, headers);
356357
}
357358

358359
if (renderMode === RenderMode.Prerender) {

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,40 @@ export function isValidRedirectResponseCode(code: number): boolean {
2727
* @param location - The URL to which the response should redirect.
2828
* @param status - The HTTP status code for the redirection. Defaults to 302 (Found).
2929
* See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
30+
* @param headers - Additional headers to include in the response.
3031
* @returns A `Response` object representing the HTTP redirect.
3132
*/
32-
export function createRedirectResponse(location: string, status = 302): Response {
33+
export function createRedirectResponse(
34+
location: string,
35+
status = 302,
36+
headers?: Record<string, string>,
37+
): Response {
3338
if (ngDevMode && !isValidRedirectResponseCode(status)) {
3439
throw new Error(
3540
`Invalid redirect status code: ${status}. ` +
3641
`Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`,
3742
);
3843
}
3944

45+
const resHeaders = new Headers(headers);
46+
if (ngDevMode && resHeaders.has('location')) {
47+
// eslint-disable-next-line no-console
48+
console.warn(
49+
`Location header "${resHeaders.get('location')}" will ignored and set to "${location}".`,
50+
);
51+
}
52+
53+
let vary = resHeaders.get('Vary') ?? '';
54+
if (vary) {
55+
vary += ', ';
56+
}
57+
vary += 'X-Forwarded-Prefix';
58+
59+
resHeaders.set('Vary', vary);
60+
resHeaders.set('Location', location);
61+
4062
return new Response(null, {
4163
status,
42-
headers: {
43-
'Location': location,
44-
},
64+
headers: resHeaders,
4565
});
4666
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { createRedirectResponse } from '../../src/utils/redirect';
10+
11+
describe('Redirect Utils', () => {
12+
describe('createRedirectResponse', () => {
13+
it('should create a redirect response with default status 302', () => {
14+
const response = createRedirectResponse('/home');
15+
expect(response.status).toBe(302);
16+
expect(response.headers.get('Location')).toBe('/home');
17+
expect(response.headers.get('Vary')).toBe('X-Forwarded-Prefix');
18+
});
19+
20+
it('should create a redirect response with a custom status', () => {
21+
const response = createRedirectResponse('/home', 301);
22+
expect(response.status).toBe(301);
23+
expect(response.headers.get('Location')).toBe('/home');
24+
});
25+
26+
it('should allow providing additional headers', () => {
27+
const response = createRedirectResponse('/home', 302, { 'X-Custom': 'value' });
28+
expect(response.headers.get('X-Custom')).toBe('value');
29+
expect(response.headers.get('Location')).toBe('/home');
30+
expect(response.headers.get('Vary')).toBe('X-Forwarded-Prefix');
31+
});
32+
33+
it('should append to Vary header instead of overriding it', () => {
34+
const response = createRedirectResponse('/home', 302, {
35+
'Location': '/evil',
36+
'Vary': 'Host',
37+
});
38+
expect(response.headers.get('Location')).toBe('/home');
39+
expect(response.headers.get('Vary')).toBe('Host, X-Forwarded-Prefix');
40+
});
41+
42+
it('should warn if Location header is provided in extra headers in dev mode', () => {
43+
// @ts-expect-error accessing global
44+
globalThis.ngDevMode = true;
45+
const warnSpy = spyOn(console, 'warn');
46+
createRedirectResponse('/home', 302, { 'Location': '/evil' });
47+
expect(warnSpy).toHaveBeenCalledWith(
48+
'Location header "/evil" will ignored and set to "/home".',
49+
);
50+
});
51+
52+
it('should throw error for invalid redirect status code in dev mode', () => {
53+
// @ts-expect-error accessing global
54+
globalThis.ngDevMode = true;
55+
expect(() => createRedirectResponse('/home', 200)).toThrowError(
56+
/Invalid redirect status code: 200/,
57+
);
58+
});
59+
});
60+
});

0 commit comments

Comments
 (0)