diff --git a/packages/angular/ssr/node/src/request.ts b/packages/angular/ssr/node/src/request.ts index f99e40491b07..32d90d0029fc 100644 --- a/packages/angular/ssr/node/src/request.ts +++ b/packages/angular/ssr/node/src/request.ts @@ -76,7 +76,7 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers { * @param nodeRequest - The Node.js `IncomingMessage` or `Http2ServerRequest` object to extract URL information from. * @returns A `URL` object representing the request URL. */ -function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): URL { +export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): URL { const { headers, socket, @@ -101,7 +101,7 @@ function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): UR } } - return new URL(originalUrl ?? url, `${protocol}://${hostnameWithPort}`); + return new URL(`${protocol}://${hostnameWithPort}${originalUrl ?? url}`); } /** diff --git a/packages/angular/ssr/node/test/request_spec.ts b/packages/angular/ssr/node/test/request_spec.ts new file mode 100644 index 000000000000..952faefd9f28 --- /dev/null +++ b/packages/angular/ssr/node/test/request_spec.ts @@ -0,0 +1,158 @@ +/** + * @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 + */ + +import { IncomingMessage } from 'node:http'; +import { Http2ServerRequest } from 'node:http2'; +import { Socket } from 'node:net'; +import { createRequestUrl } from '../src/request'; + +// Helper to create a mock request object for testing. +function createRequest(details: { + headers: Record; + encryptedSocket?: boolean; + url?: string; + originalUrl?: string; +}): IncomingMessage { + return { + headers: details.headers, + socket: details.encryptedSocket ? ({ encrypted: true } as unknown as Socket) : new Socket(), + url: details.url, + originalUrl: details.originalUrl, + } as unknown as IncomingMessage; +} + +// Helper to create a mock Http2ServerRequest object for testing. +function createHttp2Request(details: { + headers: Record; + url?: string; +}): Http2ServerRequest { + return { + headers: details.headers, + socket: new Socket(), + url: details.url, + } as Http2ServerRequest; +} + +describe('createRequestUrl', () => { + it('should create a http URL with hostname and port from the host header', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'localhost:8080' }, + url: '/test', + }), + ); + expect(url.href).toBe('http://localhost:8080/test'); + }); + + it('should create a https URL when the socket is encrypted', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'example.com' }, + encryptedSocket: true, + url: '/test', + }), + ); + expect(url.href).toBe('https://example.com/test'); + }); + + it('should use "/" as the path when the URL path is empty', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'example.com' }, + encryptedSocket: true, + url: '', + }), + ); + expect(url.href).toBe('https://example.com/'); + }); + + it('should preserve query parameters in the URL path', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'example.com' }, + encryptedSocket: true, + url: '/test?a=1', + }), + ); + expect(url.href).toBe('https://example.com/test?a=1'); + }); + + it('should prioritize "originalUrl" over "url" for the path', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'example.com' }, + encryptedSocket: true, + url: '/test', + originalUrl: '/original', + }), + ); + expect(url.href).toBe('https://example.com/original'); + }); + + it('should use "/" as the path when both "url" and "originalUrl" are not provided', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'example.com' }, + encryptedSocket: true, + url: undefined, + originalUrl: undefined, + }), + ); + expect(url.href).toBe('https://example.com/'); + }); + + it('should treat a protocol-relative value in "url" as part of the path', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'localhost:8080' }, + url: '//example.com/test', + }), + ); + expect(url.href).toBe('http://localhost:8080//example.com/test'); + }); + + it('should treat a protocol-relative value in "originalUrl" as part of the path', () => { + const url = createRequestUrl( + createRequest({ + headers: { host: 'localhost:8080' }, + url: '/test', + originalUrl: '//example.com/original', + }), + ); + expect(url.href).toBe('http://localhost:8080//example.com/original'); + }); + + it('should prioritize "x-forwarded-host" and "x-forwarded-proto" headers', () => { + const url = createRequestUrl( + createRequest({ + headers: { + host: 'localhost:8080', + 'x-forwarded-host': 'example.com', + 'x-forwarded-proto': 'https', + }, + url: '/test', + }), + ); + expect(url.href).toBe('https://example.com/test'); + }); + + it('should use "x-forwarded-port" header for the port', () => { + const url = createRequestUrl( + createRequest({ + headers: { + host: 'localhost:8080', + 'x-forwarded-host': 'example.com', + 'x-forwarded-proto': 'https', + 'x-forwarded-port': '8443', + }, + url: '/test', + }), + ); + expect(url.href).toBe('https://example.com:8443/test'); + }); +});