Skip to content

Commit ad1d7d7

Browse files
alan-agius4dgp1130
authored andcommitted
fix(@angular/ssr): ensure correct Location header for redirects behind a proxy
Previously, when the application was served behind a proxy, server-side redirects generated an incorrect Location header, causing navigation issues. This fix updates `createRequestUrl` to use the port from the Host header, ensuring accurate in proxy environments. Additionally, the Location header now only contains the pathname, improving compliance with redirect handling in such setups. Closes angular#29151
1 parent f7c0a83 commit ad1d7d7

File tree

3 files changed

+37
-14
lines changed

3 files changed

+37
-14
lines changed

packages/angular/ssr/node/src/request.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,40 @@ function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): UR
8383
originalUrl,
8484
} = nodeRequest as IncomingMessage & { originalUrl?: string };
8585
const protocol =
86-
headers['x-forwarded-proto'] ?? ('encrypted' in socket && socket.encrypted ? 'https' : 'http');
87-
const hostname = headers['x-forwarded-host'] ?? headers.host ?? headers[':authority'];
88-
const port = headers['x-forwarded-port'] ?? socket.localPort;
86+
getFirstHeaderValue(headers['x-forwarded-proto']) ??
87+
('encrypted' in socket && socket.encrypted ? 'https' : 'http');
88+
const hostname =
89+
getFirstHeaderValue(headers['x-forwarded-host']) ?? headers.host ?? headers[':authority'];
8990

9091
if (Array.isArray(hostname)) {
9192
throw new Error('host value cannot be an array.');
9293
}
9394

9495
let hostnameWithPort = hostname;
95-
if (port && !hostname?.includes(':')) {
96-
hostnameWithPort += `:${port}`;
96+
if (!hostname?.includes(':')) {
97+
const port = getFirstHeaderValue(headers['x-forwarded-port']);
98+
if (port) {
99+
hostnameWithPort += `:${port}`;
100+
}
97101
}
98102

99103
return new URL(originalUrl ?? url, `${protocol}://${hostnameWithPort}`);
100104
}
105+
106+
/**
107+
* Extracts the first value from a multi-value header string.
108+
*
109+
* @param value - A string or an array of strings representing the header values.
110+
* If it's a string, values are expected to be comma-separated.
111+
* @returns The first trimmed value from the multi-value header, or `undefined` if the input is invalid or empty.
112+
*
113+
* @example
114+
* ```typescript
115+
* getFirstHeaderValue("value1, value2, value3"); // "value1"
116+
* getFirstHeaderValue(["value1", "value2"]); // "value1"
117+
* getFirstHeaderValue(undefined); // undefined
118+
* ```
119+
*/
120+
function getFirstHeaderValue(value: string | string[] | undefined): string | undefined {
121+
return value?.toString().split(',', 1)[0]?.trim();
122+
}

packages/angular/ssr/src/app.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,15 @@ export class AngularServerApp {
161161

162162
const { redirectTo, status, renderMode } = matchedRoute;
163163
if (redirectTo !== undefined) {
164-
return Response.redirect(
165-
new URL(buildPathWithParams(redirectTo, url.pathname), url),
164+
return new Response(null, {
166165
// Note: The status code is validated during route extraction.
167166
// 302 Found is used by default for redirections
168167
// See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
169-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
170-
(status as any) ?? 302,
171-
);
168+
status: status ?? 302,
169+
headers: {
170+
'Location': buildPathWithParams(redirectTo, url.pathname),
171+
},
172+
});
172173
}
173174

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

packages/angular/ssr/test/app_spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,25 +106,25 @@ describe('AngularServerApp', () => {
106106

107107
it('should correctly handle top level redirects', async () => {
108108
const response = await app.handle(new Request('http://localhost/redirect'));
109-
expect(response?.headers.get('location')).toContain('http://localhost/home');
109+
expect(response?.headers.get('location')).toContain('/home');
110110
expect(response?.status).toBe(302);
111111
});
112112

113113
it('should correctly handle relative nested redirects', async () => {
114114
const response = await app.handle(new Request('http://localhost/redirect/relative'));
115-
expect(response?.headers.get('location')).toContain('http://localhost/redirect/home');
115+
expect(response?.headers.get('location')).toContain('/redirect/home');
116116
expect(response?.status).toBe(302);
117117
});
118118

119119
it('should correctly handle relative nested redirects with parameter', async () => {
120120
const response = await app.handle(new Request('http://localhost/redirect/param/relative'));
121-
expect(response?.headers.get('location')).toContain('http://localhost/redirect/param/home');
121+
expect(response?.headers.get('location')).toContain('/redirect/param/home');
122122
expect(response?.status).toBe(302);
123123
});
124124

125125
it('should correctly handle absolute nested redirects', async () => {
126126
const response = await app.handle(new Request('http://localhost/redirect/absolute'));
127-
expect(response?.headers.get('location')).toContain('http://localhost/home');
127+
expect(response?.headers.get('location')).toContain('/home');
128128
expect(response?.status).toBe(302);
129129
});
130130

0 commit comments

Comments
 (0)