From e1e579f3cff166bcbd2d80bc14b3882a1af85ec7 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:36:19 +0000 Subject: [PATCH] fix(@angular/build): force dev-server to use HTTP/1.1 when using SSR with SSL When using SSR with SSL, Vite attempts to use HTTP/2 by default. However, the Express server used for SSR does not support HTTP/2, causing requests to fail. This commit forces Vite to use HTTP/1.1 in this scenario by setting the ALPNProtocols to `['http/1.1']`. This is required as now Vite uses HTTP 2 for SSL: https://github.com/vitejs/vite/commit/fc21af7a42dd559a95f54b6165d34f36883eaa7f Closes #31894 --- .../src/builders/dev-server/vite/server.ts | 8 +------- .../src/tools/vite/plugins/ssr-ssl-plugin.ts | 12 ++++++++++++ tests/legacy-cli/e2e/tests/vite/ssr-with-ssl.ts | 17 +++++++++++++---- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/angular/build/src/builders/dev-server/vite/server.ts b/packages/angular/build/src/builders/dev-server/vite/server.ts index 9cabeabccfec..73f58ad5c348 100644 --- a/packages/angular/build/src/builders/dev-server/vite/server.ts +++ b/packages/angular/build/src/builders/dev-server/vite/server.ts @@ -60,13 +60,7 @@ async function createServerConfig( headers: serverOptions.headers, // Disable the websocket if live reload is disabled (false/undefined are the only valid values) ws: serverOptions.liveReload === false && serverOptions.hmr === false ? false : undefined, - // When server-side rendering (SSR) is enabled togather with SSL and Express is being used, - // we must configure Vite to use HTTP/1.1. - // This is necessary because Express does not support HTTP/2. - // We achieve this by defining an empty proxy. - // See: https://github.com/vitejs/vite/blob/c4b532cc900bf988073583511f57bd581755d5e3/packages/vite/src/node/http.ts#L106 - proxy: - serverOptions.ssl && ssrMode === ServerSsrMode.ExternalSsrMiddleware ? (proxy ?? {}) : proxy, + proxy, cors: { // This will add the header `Access-Control-Allow-Origin: http://example.com`, // where `http://example.com` is the requesting origin. diff --git a/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts b/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts index c87e0bd112a0..0cde7f89ef0a 100644 --- a/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts @@ -23,6 +23,18 @@ export function createAngularServerSideSSLPlugin(): Plugin { return; } + if (httpServer && 'ALPNProtocols' in httpServer) { + // Force Vite to use HTTP/1.1 when SSR and SSL are enabled. + // This is required because the Express server used for SSR does not support HTTP/2. + // See: https://github.com/vitejs/vite/blob/46d3077f2b63771cc50230bc907c48f5773c00fb/packages/vite/src/node/http.ts#L126 + + // We directly set the `ALPNProtocols` on the HTTP server to override the default behavior. + // Passing `ALPNProtocols` in the TLS options would cause Node.js to automatically include `h2`. + // Additionally, using `ALPNCallback` is not an option as it is mutually exclusive with `ALPNProtocols`. + // See: https://github.com/nodejs/node/blob/b8b4350ed3b73d225eb9e628d69151df56eaf298/lib/internal/http2/core.js#L3351 + httpServer.ALPNProtocols = ['http/1.1']; + } + // TODO(alanagius): Replace `undici` with `tls.setDefaultCACertificates` once we only support Node.js 22.18.0+ and 24.5.0+. // See: https://nodejs.org/api/tls.html#tlssetdefaultcacertificatescerts const { getGlobalDispatcher, setGlobalDispatcher, Agent } = await import('undici'); diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-with-ssl.ts b/tests/legacy-cli/e2e/tests/vite/ssr-with-ssl.ts index 5bb8d9105cf8..90518080f8f3 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-with-ssl.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-with-ssl.ts @@ -42,14 +42,23 @@ export default async function () { const port = await ngServe('--ssl'); - // Verify the server is running and the API response is correct. - await validateResponse('/main.js', /bootstrapApplication/); - await validateResponse('/home', /home works/); + // http 2 + await validateResponse('/main.js', /bootstrapApplication/, true); + await validateResponse('/home', /home works/, true); - async function validateResponse(pathname: string, match: RegExp): Promise { + // http 1.1 + await validateResponse('/main.js', /bootstrapApplication/, false); + await validateResponse('/home', /home works/, false); + + async function validateResponse( + pathname: string, + match: RegExp, + allowH2: boolean, + ): Promise { const response = await fetch(new URL(pathname, `https://localhost:${port}`), { dispatcher: new Agent({ connect: { + allowH2, rejectUnauthorized: false, }, }),