Skip to content

Commit cc2668f

Browse files
authored
fix(@angular/build): simplify SSL handling for ng serve with SSR (#31723)
This commit simplifies the handling of self-signed SSL certificates for `ng serve` with SSR. Previously, tests and potentially users had to set `NODE_TLS_REJECT_UNAUTHORIZED` or `NODE_EXTRA_CA_CERTS` to bypass certificate validation issues with self-signed certificates. Closes #31710 (cherry picked from commit 1850f7b)
1 parent b76962d commit cc2668f

File tree

14 files changed

+224
-76
lines changed

14 files changed

+224
-76
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
}
170170
},
171171
"resolutions": {
172-
"typescript": "5.9.3"
172+
"typescript": "5.9.3",
173+
"undici-types": "^7.16.0"
173174
}
174175
}

packages/angular/build/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ ts_project(
105105
":node_modules/sass",
106106
":node_modules/source-map-support",
107107
":node_modules/tinyglobby",
108+
":node_modules/undici",
108109
":node_modules/vite",
109110
":node_modules/vitest",
110111
":node_modules/watchpack",

packages/angular/build/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,16 @@
4242
"semver": "7.7.3",
4343
"source-map-support": "0.5.21",
4444
"tinyglobby": "0.2.15",
45+
"undici": "7.16.0",
4546
"vite": "7.1.11",
4647
"watchpack": "2.4.4"
4748
},
4849
"optionalDependencies": {
4950
"lmdb": "3.4.3"
5051
},
5152
"devDependencies": {
52-
"@angular/ssr": "workspace:*",
5353
"@angular-devkit/core": "workspace:*",
54+
"@angular/ssr": "workspace:*",
5455
"jsdom": "27.0.1",
5556
"less": "4.4.2",
5657
"ng-packagr": "21.0.0-rc.0",
@@ -59,9 +60,9 @@
5960
"vitest": "4.0.6"
6061
},
6162
"peerDependencies": {
62-
"@angular/core": "0.0.0-ANGULAR-FW-PEER-DEP",
6363
"@angular/compiler": "0.0.0-ANGULAR-FW-PEER-DEP",
6464
"@angular/compiler-cli": "0.0.0-ANGULAR-FW-PEER-DEP",
65+
"@angular/core": "0.0.0-ANGULAR-FW-PEER-DEP",
6566
"@angular/localize": "0.0.0-ANGULAR-FW-PEER-DEP",
6667
"@angular/platform-browser": "0.0.0-ANGULAR-FW-PEER-DEP",
6768
"@angular/platform-server": "0.0.0-ANGULAR-FW-PEER-DEP",

packages/angular/build/src/builders/dev-server/vite/server.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { ComponentStyleRecord } from '../../../tools/vite/middlewares';
1313
import {
1414
ServerSsrMode,
1515
createAngularMemoryPlugin,
16+
createAngularServerSideSSLPlugin,
1617
createAngularSetupMiddlewaresPlugin,
1718
createAngularSsrTransformPlugin,
1819
createRemoveIdPrefixPlugin,
@@ -207,16 +208,19 @@ export async function setupServer(
207208
preTransformRequests,
208209
cacheDir,
209210
),
210-
ssr: createSsrConfig(
211-
externalMetadata,
212-
serverOptions,
213-
prebundleTransformer,
214-
zoneless,
215-
target,
216-
prebundleLoaderExtensions,
217-
thirdPartySourcemaps,
218-
define,
219-
),
211+
ssr:
212+
ssrMode === ServerSsrMode.NoSsr
213+
? undefined
214+
: createSsrConfig(
215+
externalMetadata,
216+
serverOptions,
217+
prebundleTransformer,
218+
zoneless,
219+
target,
220+
prebundleLoaderExtensions,
221+
thirdPartySourcemaps,
222+
define,
223+
),
220224
plugins: [
221225
createAngularSetupMiddlewaresPlugin({
222226
outputFiles,
@@ -258,11 +262,15 @@ export async function setupServer(
258262
};
259263

260264
if (serverOptions.ssl) {
265+
configuration.plugins ??= [];
261266
if (!serverOptions.sslCert || !serverOptions.sslKey) {
262267
const { default: basicSslPlugin } = await import('@vitejs/plugin-basic-ssl');
263-
configuration.plugins ??= [];
264268
configuration.plugins.push(basicSslPlugin());
265269
}
270+
271+
if (ssrMode !== ServerSsrMode.NoSsr) {
272+
configuration.plugins?.push(createAngularServerSideSSLPlugin());
273+
}
266274
}
267275

268276
return configuration;

packages/angular/build/src/tools/vite/plugins/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export { createAngularMemoryPlugin } from './angular-memory-plugin';
1010
export { createRemoveIdPrefixPlugin } from './id-prefix-plugin';
1111
export { createAngularSetupMiddlewaresPlugin, ServerSsrMode } from './setup-middlewares-plugin';
1212
export { createAngularSsrTransformPlugin } from './ssr-transform-plugin';
13+
export { createAngularServerSideSSLPlugin } from './ssr-ssl-plugin';
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 { rootCertificates } from 'node:tls';
10+
import type { Plugin } from 'vite';
11+
12+
export function createAngularServerSideSSLPlugin(): Plugin {
13+
return {
14+
name: 'angular-ssr-ssl-plugin',
15+
apply: 'serve',
16+
async configureServer({ config, httpServer }) {
17+
const {
18+
ssr,
19+
server: { https },
20+
} = config;
21+
22+
if (!ssr || !https?.cert) {
23+
return;
24+
}
25+
26+
// TODO(alanagius): Replace `undici` with `tls.setDefaultCACertificates` once we only support Node.js 22.18.0+ and 24.5.0+.
27+
// See: https://nodejs.org/api/tls.html#tlssetdefaultcacertificatescerts
28+
const { getGlobalDispatcher, setGlobalDispatcher, Agent } = await import('undici');
29+
const originalDispatcher = getGlobalDispatcher();
30+
const { cert } = https;
31+
const certificates = Array.isArray(cert) ? cert : [cert];
32+
33+
setGlobalDispatcher(
34+
new Agent({
35+
connect: {
36+
ca: [...rootCertificates, ...certificates],
37+
},
38+
}),
39+
);
40+
41+
httpServer?.on('close', () => {
42+
setGlobalDispatcher(originalDispatcher);
43+
});
44+
},
45+
};
46+
}

packages/angular_devkit/build_angular/src/builders/dev-server/specs/ssl_spec.ts

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { Architect, BuilderRun } from '@angular-devkit/architect';
1010
import { tags } from '@angular-devkit/core';
11-
import { Agent, getGlobalDispatcher, setGlobalDispatcher } from 'undici';
11+
import { Agent } from 'undici';
1212
import { createArchitect, host } from '../../../testing/test-utils';
1313
import { DevServerBuilderOutput } from '../index';
1414

@@ -35,20 +35,12 @@ describe('Dev Server Builder ssl', () => {
3535
expect(output.success).toBe(true);
3636
expect(output.baseUrl).toMatch(/^https:\/\/localhost:\d+\//);
3737

38-
// The self-signed certificate used by the dev server will cause fetch to fail
39-
// unless reject unauthorized is disabled.
40-
const originalDispatcher = getGlobalDispatcher();
41-
setGlobalDispatcher(
42-
new Agent({
38+
const response = await fetch(output.baseUrl, {
39+
dispatcher: new Agent({
4340
connect: { rejectUnauthorized: false },
4441
}),
45-
);
46-
try {
47-
const response = await fetch(output.baseUrl);
48-
expect(await response.text()).toContain('<title>HelloWorldApp</title>');
49-
} finally {
50-
setGlobalDispatcher(originalDispatcher);
51-
}
42+
});
43+
expect(await response.text()).toContain('<title>HelloWorldApp</title>');
5244
});
5345

5446
it('supports key and cert', async () => {
@@ -122,19 +114,11 @@ describe('Dev Server Builder ssl', () => {
122114
expect(output.success).toBe(true);
123115
expect(output.baseUrl).toMatch(/^https:\/\/localhost:\d+\//);
124116

125-
// The self-signed certificate used by the dev server will cause fetch to fail
126-
// unless reject unauthorized is disabled.
127-
const originalDispatcher = getGlobalDispatcher();
128-
setGlobalDispatcher(
129-
new Agent({
117+
const response = await fetch(output.baseUrl, {
118+
dispatcher: new Agent({
130119
connect: { rejectUnauthorized: false },
131120
}),
132-
);
133-
try {
134-
const response = await fetch(output.baseUrl);
135-
expect(await response.text()).toContain('<title>HelloWorldApp</title>');
136-
} finally {
137-
setGlobalDispatcher(originalDispatcher);
138-
}
121+
});
122+
expect(await response.text()).toContain('<title>HelloWorldApp</title>');
139123
});
140124
});

packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { Architect } from '@angular-devkit/architect';
1010
// eslint-disable-next-line import/no-extraneous-dependencies
1111
import * as browserSync from 'browser-sync';
12-
import { Agent, getGlobalDispatcher, setGlobalDispatcher } from 'undici';
12+
import { Agent } from 'undici';
1313
import { createArchitect, host } from '../../../testing/test-utils';
1414
import { SSRDevServerBuilderOutput } from '../index';
1515

@@ -85,20 +85,13 @@ describe('Serve SSR Builder', () => {
8585
expect(output.success).toBe(true);
8686
expect(output.baseUrl).toBe(`https://localhost:${output.port}`);
8787

88-
// The self-signed certificate used by the dev server will cause fetch to fail
89-
// unless reject unauthorized is disabled.
90-
const originalDispatcher = getGlobalDispatcher();
91-
setGlobalDispatcher(
92-
new Agent({
88+
const response = await fetch(`https://localhost:${output.port}/index.html`, {
89+
dispatcher: new Agent({
9390
connect: { rejectUnauthorized: false },
9491
}),
95-
);
96-
try {
97-
const response = await fetch(`https://localhost:${output.port}/index.html`);
98-
expect(await response.text()).toContain('<title>HelloWorldApp</title>');
99-
} finally {
100-
setGlobalDispatcher(originalDispatcher);
101-
}
92+
});
93+
94+
expect(await response.text()).toContain('<title>HelloWorldApp</title>');
10295

10396
await run.stop();
10497
});

pnpm-lock.yaml

Lines changed: 5 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/legacy-cli/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ e2e_suites(
6868
# Extra runtime deps due to bundling issues.
6969
# TODO: Clean this up.
7070
"//:node_modules/express",
71+
"//:node_modules/undici",
7172
],
7273
runner = ":runner_entrypoint",
7374
)

0 commit comments

Comments
 (0)