@@ -81,6 +81,16 @@ interface HttpCliAttempt {
8181 close : ( ) => Promise < void > ;
8282}
8383
84+ class HttpCliStartupError extends Error {
85+ readonly code ?: string ;
86+
87+ constructor ( message : string , readonly startupStderr : string , code ?: string ) {
88+ super ( message ) ;
89+ this . name = 'HttpCliStartupError' ;
90+ this . code = code ;
91+ }
92+ }
93+
8494const MAX_HTTP_CLI_START_ATTEMPTS = 2 ;
8595const MAX_CAPTURED_OUTPUT_CHARS = 64 * 1024 ;
8696const MAX_HEALTH_RESPONSE_CHARS = 16 * 1024 ;
@@ -251,21 +261,32 @@ async function captureCleanupFailure(close: () => Promise<void>): Promise<string
251261 }
252262}
253263
264+ function getErrorCode ( error : Error ) : string | undefined {
265+ const code = ( error as Error & { code ?: unknown } ) . code ;
266+ return typeof code === 'string' ? code : undefined ;
267+ }
268+
254269async function startHttpCliAttempt ( port : number , config : HttpCliConfig ) : Promise < SpawnedHttpCli > {
255270 const attempt = createHttpCliAttempt ( port , config ) ;
256271 try {
257272 const health = await waitForHealth ( attempt . child , port , attempt . output , config . readyTimeoutMs ) ;
258273 return { url : attempt . url , health, close : attempt . close } ;
259274 } catch ( error ) {
260275 const message = error instanceof Error ? error . message : String ( error ) ;
276+ const code = error instanceof Error ? getErrorCode ( error ) : undefined ;
261277 const cleanupFailure = await captureCleanupFailure ( attempt . close ) ;
262278 const cleanupError = cleanupFailure ? `; cleanupFailure=${ JSON . stringify ( cleanupFailure ) } ` : '' ;
263- throw new Error ( `${ message } ; cleanup=${ diagnostics ( attempt . child , attempt . output ) } ${ cleanupError } ` ) ;
279+ throw new HttpCliStartupError (
280+ `${ message } ; cleanup=${ diagnostics ( attempt . child , attempt . output ) } ${ cleanupError } ` ,
281+ attempt . output . stderr ,
282+ code ,
283+ ) ;
264284 }
265285}
266286
267287function isAddressInUse ( error : Error ) : boolean {
268- return error . message . includes ( 'EADDRINUSE' ) ;
288+ return getErrorCode ( error ) === 'EADDRINUSE'
289+ || error instanceof HttpCliStartupError && error . startupStderr . includes ( 'EADDRINUSE' ) ;
269290}
270291
271292/** Starts the built CLI on an isolated loopback port and waits for /health. */
0 commit comments