@@ -55,6 +55,57 @@ interface AngularSetupMiddlewaresPluginOptions {
5555 ssrMode : ServerSsrMode ;
5656 resetComponentUpdates : ( ) => void ;
5757 projectRoot : string ;
58+ allowedHosts : true | string [ ] ;
59+ devHost : string ;
60+ }
61+
62+ function extractHostname ( hostHeader : string | undefined ) : string {
63+ if ( ! hostHeader ) {
64+ return '' ;
65+ }
66+
67+ // Remove port if present (e.g., example.com:4200)
68+ const idx = hostHeader . lastIndexOf ( ':' ) ;
69+ if ( idx > - 1 && hostHeader . indexOf ( ']' ) === - 1 ) {
70+ // Skip IPv6 addresses that include ':' within brackets
71+ return hostHeader . slice ( 0 , idx ) . toLowerCase ( ) ;
72+ }
73+
74+ return hostHeader . toLowerCase ( ) ;
75+ }
76+
77+ function html403 ( hostname : string ) : string {
78+ return `<!doctype html>
79+ <html>
80+ <head>
81+ <meta charset="utf-8" />
82+ <meta name="viewport" content="width=device-width, initial-scale=1" />
83+ <title>Blocked request</title>
84+ <style>
85+ body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;
86+ line-height:1.4;margin:2rem;color:#1f2937}
87+ code{background:#f3f4f6;padding:.15rem .35rem;border-radius:.25rem}
88+ .box{max-width:760px;margin:0 auto}
89+ h1{font-size:1.5rem;margin-bottom:.75rem}
90+ p{margin:.5rem 0}
91+ .muted{color:#6b7280}
92+ pre{background:#f9fafb;border:1px solid #e5e7eb;padding:.75rem;border-radius:.5rem;overflow:auto}
93+ </style>
94+ </head>
95+ <body>
96+ <main>
97+ <h1>Blocked request. This host ("${ hostname } ") is not allowed.</h1>
98+ <p>To allow this host, add it to <code>allowedHosts</code> under the <code>serve</code> target in <code>angular.json</code>.</p>
99+ <pre><code>{
100+ "serve": {
101+ "options": {
102+ "allowedHosts": ["${ hostname } "]
103+ }
104+ }
105+ }</code></pre>
106+ </main>
107+ </body>
108+ </html>` ;
58109}
59110
60111async function createEncapsulateStyle ( ) : Promise <
@@ -109,6 +160,88 @@ export function createAngularSetupMiddlewaresPlugin(
109160 // before the built-in HTML middleware
110161 // eslint-disable-next-line @typescript-eslint/no-misused-promises
111162 return async ( ) => {
163+ type MiddlewareStackEntry = {
164+ route ?: string ;
165+ handle ?: ( Connect . NextHandleFunction & { name ?: string } ) | undefined ;
166+ } ;
167+
168+ // Vite/Connect do not expose a typed stack, cast once to a precise structural type.
169+ const mws = server . middlewares as unknown as { stack : MiddlewareStackEntry [ ] } ;
170+ const middlewareStack = mws . stack ;
171+
172+ // Helper to locate Vite's host validation middleware safely
173+ const findHostValidationEntry = (
174+ stack : readonly MiddlewareStackEntry [ ] ,
175+ ) : MiddlewareStackEntry | undefined =>
176+ stack . find (
177+ ( { handle } ) =>
178+ typeof handle === 'function' &&
179+ typeof handle . name === 'string' &&
180+ handle . name . startsWith ( 'hostValidationMiddleware' ) ,
181+ ) ;
182+
183+ const entry = findHostValidationEntry ( middlewareStack ) ;
184+
185+ if ( entry && typeof entry . handle === 'function' ) {
186+ const originalHandle = entry . handle as Connect . NextHandleFunction ;
187+
188+ entry . handle = function angularHostValidationMiddleware ( req , res , next ) {
189+ const originalWriteHead = res . writeHead . bind ( res ) ;
190+ const originalEnd = res . end . bind ( res ) ;
191+ const originalWrite = res . write . bind ( res ) ;
192+
193+ let blocked = false ;
194+ let capturedStatus : number | undefined ;
195+
196+ // Intercept writeHead: detect block and delay header sending
197+ res . writeHead = function ( ...args : Parameters < typeof res . writeHead > ) {
198+ const statusCode = args [ 0 ] ;
199+ capturedStatus = statusCode ;
200+ if ( typeof statusCode === 'number' && statusCode >= 400 ) {
201+ blocked = true ;
202+
203+ return res ;
204+ }
205+
206+ return originalWriteHead . apply ( res , args ) ;
207+ } as typeof res . writeHead ;
208+
209+ // Intercept write to avoid implicitly sending headers/body when blocked
210+ res . write = function (
211+ ...args : Parameters < typeof res . write >
212+ ) : ReturnType < typeof res . write > {
213+ if ( blocked ) {
214+ // Swallow writes to prevent Node from implicitly sending headers
215+ return true as ReturnType < typeof res . write > ;
216+ }
217+
218+ return originalWrite . apply ( res , args ) ;
219+ } as typeof res . write ;
220+
221+ res . end = function ( ...args : Parameters < typeof res . end > ) : ReturnType < typeof res . end > {
222+ if ( blocked ) {
223+ const hostname = extractHostname ( req . headers . host ) ;
224+ const body = html403 ( hostname ) ;
225+ const status = capturedStatus && capturedStatus >= 400 ? capturedStatus : 403 ;
226+
227+ // Headers were not sent yet because we intercepted writeHead for blocked case
228+ if ( ! res . headersSent ) {
229+ const headers : import ( 'node:http' ) . OutgoingHttpHeaders = {
230+ 'Content-Type' : 'text/html; charset=utf-8' ,
231+ } ;
232+ originalWriteHead ( status , headers ) ;
233+ }
234+
235+ return originalEnd ( body ) ;
236+ }
237+
238+ return originalEnd . apply ( res , args ) ;
239+ } as typeof res . end ;
240+
241+ return originalHandle ( req , res , next ) ;
242+ } ;
243+ }
244+
112245 if ( ssrMode === ServerSsrMode . ExternalSsrMiddleware ) {
113246 server . middlewares . use (
114247 await createAngularSsrExternalMiddleware ( server , indexHtmlTransformer ) ,
0 commit comments