Skip to content

Commit 03337ee

Browse files
committed
fix(@angular/build): The error message generated when serve angular app by custom hostname
Add custom middleware for to present an Angular-tailored message Fix #32028
1 parent 588f3b8 commit 03337ee

File tree

5 files changed

+135
-3
lines changed

5 files changed

+135
-3
lines changed

packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT
2727
...BASE_OPTIONS,
2828
});
2929

30-
const { result, response } = await executeOnceAndGet(harness, '/', {
30+
const { result, response, content } = await executeOnceAndGet(harness, '/', {
3131
request: { headers: FETCH_HEADERS },
3232
});
3333

3434
expect(result?.success).toBeTrue();
3535
expect(response?.statusCode).toBe(403);
36+
expect(content).toContain('angular.json');
3637
});
3738

3839
it('does not allow an invalid host when option is an empty array', async () => {
@@ -41,12 +42,13 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT
4142
allowedHosts: [],
4243
});
4344

44-
const { result, response } = await executeOnceAndGet(harness, '/', {
45+
const { result, response, content } = await executeOnceAndGet(harness, '/', {
4546
request: { headers: FETCH_HEADERS },
4647
});
4748

4849
expect(result?.success).toBeTrue();
4950
expect(response?.statusCode).toBe(403);
51+
expect(content).toContain('angular.json');
5052
});
5153

5254
it('allows a host when specified in the option', async () => {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ async function createServerConfig(
5656
strictPort: true,
5757
host: serverOptions.host,
5858
open: serverOptions.open,
59-
allowedHosts: serverOptions.allowedHosts,
59+
// Disable Vite's built-in allowed hosts page so we can present
60+
// an Angular-tailored message and keep behavior consistent with CLI options.
61+
// Re-implement the host check via a custom middleware.
62+
allowedHosts: true,
6063
headers: serverOptions.headers,
6164
// Disable the websocket if live reload is disabled (false/undefined are the only valid values)
6265
ws: serverOptions.liveReload === false && serverOptions.hmr === false ? false : undefined,
@@ -226,6 +229,8 @@ export async function setupServer(
226229
ssrMode,
227230
resetComponentUpdates: () => templateUpdates.clear(),
228231
projectRoot: serverOptions.projectRoot,
232+
allowedHosts: serverOptions.allowedHosts,
233+
devHost: serverOptions.host,
229234
}),
230235
createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser),
231236
await createAngularSsrTransformPlugin(serverOptions.workspaceRoot),
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 type { ServerResponse } from 'node:http';
10+
import type { Connect, ViteDevServer } from 'vite';
11+
12+
function extractHostname(hostHeader: string | undefined): string | undefined {
13+
if (!hostHeader) {
14+
return undefined;
15+
}
16+
17+
// Remove port if present (e.g., example.com:4200)
18+
const idx = hostHeader.lastIndexOf(':');
19+
if (idx > -1 && hostHeader.indexOf(']') === -1) {
20+
// Skip IPv6 addresses that include ':' within brackets
21+
return hostHeader.slice(0, idx).toLowerCase();
22+
}
23+
24+
return hostHeader.toLowerCase();
25+
}
26+
27+
function isLocalHost(name: string | undefined): boolean {
28+
if (!name) return false;
29+
return name === 'localhost' || name === '127.0.0.1' || name === '[::1]' || name === '::1';
30+
}
31+
32+
function html403(hostname: string): string {
33+
return `<!doctype html>
34+
<html>
35+
<head>
36+
<meta charset="utf-8" />
37+
<meta name="viewport" content="width=device-width, initial-scale=1" />
38+
<title>Blocked request</title>
39+
<style>
40+
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;line-height:1.4;margin:2rem;color:#1f2937}
41+
code{background:#f3f4f6;padding:.15rem .35rem;border-radius:.25rem}
42+
.box{max-width:760px;margin:0 auto}
43+
h1{font-size:1.5rem;margin-bottom:.75rem}
44+
p{margin:.5rem 0}
45+
.muted{color:#6b7280}
46+
pre{background:#f9fafb;border:1px solid #e5e7eb;padding:.75rem;border-radius:.5rem;overflow:auto}
47+
</style>
48+
</head>
49+
<body>
50+
<div class="box">
51+
<h1>Blocked request. This host ("${hostname}") is not allowed.</h1>
52+
<p>The Angular development server only responds to local hosts by default.</p>
53+
<p>To allow this host, add it to <code>allowedHosts</code> under the <code>serve</code> target in <code>angular.json</code>.</p>
54+
<pre><code>{
55+
"serve": {
56+
"options": {
57+
"allowedHosts": ["${hostname}"]
58+
}
59+
}
60+
}</code></pre>
61+
</div>
62+
</body>
63+
</html>`;
64+
}
65+
66+
/**
67+
* Middleware that enforces host checking using Angular CLI's `allowedHosts` option.
68+
*
69+
* Vite's own host check is disabled in the server configuration so that we can
70+
* present an Angular-specific guidance page when blocked.
71+
*/
72+
export function createAngularHostCheckMiddleware(
73+
_server: ViteDevServer,
74+
allowedHosts: true | string[] | undefined,
75+
devHost: string,
76+
): Connect.NextHandleFunction {
77+
// Normalize configured allowed hosts
78+
const allowAll = allowedHosts === true;
79+
const allowedSet = new Set(
80+
Array.isArray(allowedHosts) ? allowedHosts.map((h) => h.toLowerCase()) : [],
81+
);
82+
83+
const devHostLower = devHost?.toLowerCase?.();
84+
85+
return function angularHostCheckMiddleware(
86+
req: Connect.IncomingMessage,
87+
res: ServerResponse,
88+
next: Connect.NextFunction,
89+
) {
90+
if (allowAll) {
91+
return next();
92+
}
93+
94+
const hostname = extractHostname(req.headers.host);
95+
96+
// Always allow local access and the explicit dev host when meaningful
97+
if (
98+
isLocalHost(hostname) ||
99+
(devHostLower && devHostLower !== '0.0.0.0' && hostname === devHostLower)
100+
) {
101+
return next();
102+
}
103+
104+
// Allow if present in configured list
105+
if (hostname && allowedSet.has(hostname)) {
106+
return next();
107+
}
108+
109+
// Block with an Angular-specific 403 page
110+
const body = html403(hostname ?? '');
111+
res.statusCode = 403;
112+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
113+
res.setHeader('Content-Length', Buffer.byteLength(body));
114+
res.end(body);
115+
};
116+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ export {
1616
export { createAngularHeadersMiddleware } from './headers-middleware';
1717
export { createAngularComponentMiddleware } from './component-middleware';
1818
export { createChromeDevtoolsMiddleware } from './chrome-devtools-middleware';
19+
export { createAngularHostCheckMiddleware } from './host-check-middleware';

packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
createAngularComponentMiddleware,
1515
createAngularHeadersMiddleware,
1616
createAngularIndexHtmlMiddleware,
17+
createAngularHostCheckMiddleware,
1718
createAngularSsrExternalMiddleware,
1819
createAngularSsrInternalMiddleware,
1920
createChromeDevtoolsMiddleware,
@@ -55,6 +56,8 @@ interface AngularSetupMiddlewaresPluginOptions {
5556
ssrMode: ServerSsrMode;
5657
resetComponentUpdates: () => void;
5758
projectRoot: string;
59+
allowedHosts: true | string[];
60+
devHost: string;
5861
}
5962

6063
async function createEncapsulateStyle(): Promise<
@@ -86,6 +89,11 @@ export function createAngularSetupMiddlewaresPlugin(
8689
resetComponentUpdates,
8790
} = options;
8891

92+
// Install host check first to ensure blocked hosts receive a tailored HTML message.
93+
server.middlewares.use(
94+
createAngularHostCheckMiddleware(server, options.allowedHosts, options.devHost),
95+
);
96+
8997
// Headers, assets and resources get handled first
9098
server.middlewares.use(createAngularHeadersMiddleware(server));
9199
server.middlewares.use(createAngularComponentMiddleware(server, templateUpdates));

0 commit comments

Comments
 (0)