From af24a020dd48ee4bd8cd55a107c2788af099c9bc Mon Sep 17 00:00:00 2001 From: aparziale Date: Tue, 9 Dec 2025 17:29:35 +0100 Subject: [PATCH 1/5] fix(@angular/build): Add custom middleware for to present an Angular-tailored message New Blocked request page for custom host Fix #32028 --- .../tests/options/allowed-hosts_spec.ts | 3 ++ .../src/builders/dev-server/vite/server.ts | 2 + .../vite/middlewares/host-check-middleware.ts | 41 +++++++++++++++++++ .../build/src/tools/vite/middlewares/index.ts | 1 + .../vite/plugins/setup-middlewares-plugin.ts | 34 +++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts diff --git a/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts b/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts index 8e96c7b4b4b0..b2238d4a1142 100644 --- a/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts @@ -10,6 +10,7 @@ import { executeDevServer } from '../../index'; import { executeOnceAndGet } from '../execute-fetch'; import { describeServeBuilder } from '../jasmine-helpers'; import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { text } from 'node:stream/consumers'; const FETCH_HEADERS = Object.freeze({ Host: 'example.com' }); @@ -33,6 +34,7 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expect(result?.success).toBeTrue(); expect(response?.statusCode).toBe(403); + expect(response && text(response)).toContain('angular.json'); }); it('does not allow an invalid host when option is an empty array', async () => { @@ -47,6 +49,7 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expect(result?.success).toBeTrue(); expect(response?.statusCode).toBe(403); + expect(response && text(response)).toContain('angular.json'); }); it('allows a host when specified in the option', async () => { 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 73f58ad5c348..b60df022647c 100644 --- a/packages/angular/build/src/builders/dev-server/vite/server.ts +++ b/packages/angular/build/src/builders/dev-server/vite/server.ts @@ -226,6 +226,8 @@ export async function setupServer( ssrMode, resetComponentUpdates: () => templateUpdates.clear(), projectRoot: serverOptions.projectRoot, + allowedHosts: serverOptions.allowedHosts, + devHost: serverOptions.host, }), createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser), await createAngularSsrTransformPlugin(serverOptions.workspaceRoot), diff --git a/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts new file mode 100644 index 000000000000..e5a0adea9537 --- /dev/null +++ b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts @@ -0,0 +1,41 @@ +/** + * @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 + */ + +export function html403(hostname: string): string { + return ` + + + + + Blocked request + + + +
+

Blocked request. This host ("${hostname}") is not allowed.

+

To allow this host, add it to allowedHosts under the serve target in angular.json.

+
{
+  "serve": {
+    "options": {
+      "allowedHosts": ["${hostname}"]
+    }
+  }
+}
+
+ + `; +} diff --git a/packages/angular/build/src/tools/vite/middlewares/index.ts b/packages/angular/build/src/tools/vite/middlewares/index.ts index ef2db01f3aaf..2e57985fb9f1 100644 --- a/packages/angular/build/src/tools/vite/middlewares/index.ts +++ b/packages/angular/build/src/tools/vite/middlewares/index.ts @@ -16,3 +16,4 @@ export { export { createAngularHeadersMiddleware } from './headers-middleware'; export { createAngularComponentMiddleware } from './component-middleware'; export { createChromeDevtoolsMiddleware } from './chrome-devtools-middleware'; +export { html403 } from './host-check-middleware'; diff --git a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts index b82cc2d3acd6..fbf465b5d2b2 100644 --- a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import { IncomingMessage, ServerResponse } from 'node:http'; import type { Connect, Plugin } from 'vite'; import { ComponentStyleRecord, @@ -17,6 +18,7 @@ import { createAngularSsrExternalMiddleware, createAngularSsrInternalMiddleware, createChromeDevtoolsMiddleware, + html403, } from '../middlewares'; import { AngularMemoryOutputFiles, AngularOutputAssets } from '../utils'; @@ -55,6 +57,8 @@ interface AngularSetupMiddlewaresPluginOptions { ssrMode: ServerSsrMode; resetComponentUpdates: () => void; projectRoot: string; + allowedHosts: true | string[]; + devHost: string; } async function createEncapsulateStyle(): Promise< @@ -109,6 +113,36 @@ export function createAngularSetupMiddlewaresPlugin( // before the built-in HTML middleware // eslint-disable-next-line @typescript-eslint/no-misused-promises return async () => { + // Vite/Connect do not expose a typed stack, cast once to a precise structural type. + const entry = server.middlewares.stack.find( + ({ handle }) => + typeof handle === 'function' && handle.name.startsWith('hostValidationMiddleware'), + ); + + if (typeof entry?.handle === 'function') { + const originalHandle = entry.handle as Connect.NextHandleFunction; + + entry.handle = function angularHostValidationMiddleware( + req: IncomingMessage, + res: ServerResponse, + next: (err?: unknown) => void, + ) { + originalHandle( + req, + { + writeHead: (code) => { + res.writeHead(code, { 'content-type': 'text/html' }); + }, + end: () => { + const hostname = req.headers.host?.toLowerCase().split(':')[0] ?? ''; + res.end(html403(hostname)); + }, + } as ServerResponse, + next, + ); + }; + } + if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) { server.middlewares.use( await createAngularSsrExternalMiddleware(server, indexHtmlTransformer), From a517beb1b9b764ac7b59ca81984630b9efbd6918 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:00:46 +0000 Subject: [PATCH 2/5] fixup! fix(@angular/build): Add custom middleware for to present an Angular-tailored message --- .../vite/middlewares/host-check-middleware.ts | 38 ++++++++++++++++++- .../build/src/tools/vite/middlewares/index.ts | 2 +- .../vite/plugins/setup-middlewares-plugin.ts | 33 +--------------- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts index e5a0adea9537..a441165b3a2a 100644 --- a/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts @@ -6,7 +6,43 @@ * found in the LICENSE file at https://angular.dev/license */ -export function html403(hostname: string): string { +import type { IncomingMessage, ServerResponse } from 'node:http'; +import type { Connect } from 'vite'; + +export function createAngularHostCheckMiddleware(middlewares: Connect.Server): void { + const entry = middlewares.stack.find( + ({ handle }) => + typeof handle === 'function' && handle.name.startsWith('hostValidationMiddleware'), + ); + + if (typeof entry?.handle !== 'function') { + return; + } + + const originalHandle = entry.handle as Connect.NextHandleFunction; + + entry.handle = function angularHostValidationMiddleware( + req: IncomingMessage, + res: ServerResponse, + next: (err?: unknown) => void, + ) { + originalHandle( + req, + { + writeHead: (code) => { + res.writeHead(code, { 'content-type': 'text/html' }); + }, + end: () => { + const hostname = req.headers.host?.toLowerCase().split(':')[0] ?? ''; + res.end(html403(hostname)); + }, + } as ServerResponse, + next, + ); + }; +} + +function html403(hostname: string): string { return ` diff --git a/packages/angular/build/src/tools/vite/middlewares/index.ts b/packages/angular/build/src/tools/vite/middlewares/index.ts index 2e57985fb9f1..7cdebcfad370 100644 --- a/packages/angular/build/src/tools/vite/middlewares/index.ts +++ b/packages/angular/build/src/tools/vite/middlewares/index.ts @@ -16,4 +16,4 @@ export { export { createAngularHeadersMiddleware } from './headers-middleware'; export { createAngularComponentMiddleware } from './component-middleware'; export { createChromeDevtoolsMiddleware } from './chrome-devtools-middleware'; -export { html403 } from './host-check-middleware'; +export { createAngularHostCheckMiddleware } from './host-check-middleware'; diff --git a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts index fbf465b5d2b2..4cc5e835cd14 100644 --- a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { IncomingMessage, ServerResponse } from 'node:http'; import type { Connect, Plugin } from 'vite'; import { ComponentStyleRecord, @@ -14,11 +13,11 @@ import { createAngularAssetsMiddleware, createAngularComponentMiddleware, createAngularHeadersMiddleware, + createAngularHostCheckMiddleware, createAngularIndexHtmlMiddleware, createAngularSsrExternalMiddleware, createAngularSsrInternalMiddleware, createChromeDevtoolsMiddleware, - html403, } from '../middlewares'; import { AngularMemoryOutputFiles, AngularOutputAssets } from '../utils'; @@ -113,35 +112,7 @@ export function createAngularSetupMiddlewaresPlugin( // before the built-in HTML middleware // eslint-disable-next-line @typescript-eslint/no-misused-promises return async () => { - // Vite/Connect do not expose a typed stack, cast once to a precise structural type. - const entry = server.middlewares.stack.find( - ({ handle }) => - typeof handle === 'function' && handle.name.startsWith('hostValidationMiddleware'), - ); - - if (typeof entry?.handle === 'function') { - const originalHandle = entry.handle as Connect.NextHandleFunction; - - entry.handle = function angularHostValidationMiddleware( - req: IncomingMessage, - res: ServerResponse, - next: (err?: unknown) => void, - ) { - originalHandle( - req, - { - writeHead: (code) => { - res.writeHead(code, { 'content-type': 'text/html' }); - }, - end: () => { - const hostname = req.headers.host?.toLowerCase().split(':')[0] ?? ''; - res.end(html403(hostname)); - }, - } as ServerResponse, - next, - ); - }; - } + createAngularHostCheckMiddleware(server.middlewares); if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) { server.middlewares.use( From c587d0b5eb6c2f64fbca301f70ab1c6cecd7c274 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:03:36 +0000 Subject: [PATCH 3/5] fixup! fix(@angular/build): Add custom middleware for to present an Angular-tailored message --- .../angular/build/src/builders/dev-server/vite/server.ts | 2 -- .../src/tools/vite/middlewares/host-check-middleware.ts | 2 +- packages/angular/build/src/tools/vite/middlewares/index.ts | 2 +- .../src/tools/vite/plugins/setup-middlewares-plugin.ts | 6 ++---- 4 files changed, 4 insertions(+), 8 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 b60df022647c..73f58ad5c348 100644 --- a/packages/angular/build/src/builders/dev-server/vite/server.ts +++ b/packages/angular/build/src/builders/dev-server/vite/server.ts @@ -226,8 +226,6 @@ export async function setupServer( ssrMode, resetComponentUpdates: () => templateUpdates.clear(), projectRoot: serverOptions.projectRoot, - allowedHosts: serverOptions.allowedHosts, - devHost: serverOptions.host, }), createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser), await createAngularSsrTransformPlugin(serverOptions.workspaceRoot), diff --git a/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts index a441165b3a2a..4bc569e9b842 100644 --- a/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts @@ -9,7 +9,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; import type { Connect } from 'vite'; -export function createAngularHostCheckMiddleware(middlewares: Connect.Server): void { +export function patchHostValidationMiddleware(middlewares: Connect.Server): void { const entry = middlewares.stack.find( ({ handle }) => typeof handle === 'function' && handle.name.startsWith('hostValidationMiddleware'), diff --git a/packages/angular/build/src/tools/vite/middlewares/index.ts b/packages/angular/build/src/tools/vite/middlewares/index.ts index 7cdebcfad370..1816fe26265c 100644 --- a/packages/angular/build/src/tools/vite/middlewares/index.ts +++ b/packages/angular/build/src/tools/vite/middlewares/index.ts @@ -16,4 +16,4 @@ export { export { createAngularHeadersMiddleware } from './headers-middleware'; export { createAngularComponentMiddleware } from './component-middleware'; export { createChromeDevtoolsMiddleware } from './chrome-devtools-middleware'; -export { createAngularHostCheckMiddleware } from './host-check-middleware'; +export { patchHostValidationMiddleware } from './host-check-middleware'; diff --git a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts index 4cc5e835cd14..b14c2b409012 100644 --- a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts @@ -13,11 +13,11 @@ import { createAngularAssetsMiddleware, createAngularComponentMiddleware, createAngularHeadersMiddleware, - createAngularHostCheckMiddleware, createAngularIndexHtmlMiddleware, createAngularSsrExternalMiddleware, createAngularSsrInternalMiddleware, createChromeDevtoolsMiddleware, + patchHostValidationMiddleware, } from '../middlewares'; import { AngularMemoryOutputFiles, AngularOutputAssets } from '../utils'; @@ -56,8 +56,6 @@ interface AngularSetupMiddlewaresPluginOptions { ssrMode: ServerSsrMode; resetComponentUpdates: () => void; projectRoot: string; - allowedHosts: true | string[]; - devHost: string; } async function createEncapsulateStyle(): Promise< @@ -112,7 +110,7 @@ export function createAngularSetupMiddlewaresPlugin( // before the built-in HTML middleware // eslint-disable-next-line @typescript-eslint/no-misused-promises return async () => { - createAngularHostCheckMiddleware(server.middlewares); + patchHostValidationMiddleware(server.middlewares); if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) { server.middlewares.use( From 8e56cbbf44f2e0b13b37aaaa27730d14aa579cd6 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:09:12 +0000 Subject: [PATCH 4/5] fixup! fix(@angular/build): Add custom middleware for to present an Angular-tailored message --- .../build/src/tools/vite/middlewares/host-check-middleware.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts index 4bc569e9b842..8561354812b3 100644 --- a/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts @@ -53,10 +53,9 @@ function html403(hostname: string): string { body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif; line-height:1.4;margin:2rem;color:#1f2937} code{background:#f3f4f6;padding:.15rem .35rem;border-radius:.25rem} - .box{max-width:760px;margin:0 auto} + main{max-width:760px;margin:0 auto} h1{font-size:1.5rem;margin-bottom:.75rem} p{margin:.5rem 0} - .muted{color:#6b7280} pre{background:#f9fafb;border:1px solid #e5e7eb;padding:.75rem;border-radius:.5rem;overflow:auto} From 49a33e6f6e817a9167ee1dc43d878f3e83c43870 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:26:41 +0000 Subject: [PATCH 5/5] fixup! fix(@angular/build): Add custom middleware for to present an Angular-tailored message --- .../builders/dev-server/tests/options/allowed-hosts_spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts b/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts index b2238d4a1142..775e057bece6 100644 --- a/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts @@ -34,7 +34,7 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expect(result?.success).toBeTrue(); expect(response?.statusCode).toBe(403); - expect(response && text(response)).toContain('angular.json'); + expect(response && (await text(response))).toContain('angular.json'); }); it('does not allow an invalid host when option is an empty array', async () => { @@ -49,7 +49,7 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expect(result?.success).toBeTrue(); expect(response?.statusCode).toBe(403); - expect(response && text(response)).toContain('angular.json'); + expect(response && (await text(response))).toContain('angular.json'); }); it('allows a host when specified in the option', async () => {