diff --git a/packages/cli/src/ui/utils/updateCheck.test.ts b/packages/cli/src/ui/utils/updateCheck.test.ts index 75aa463a331..1361add9df7 100644 --- a/packages/cli/src/ui/utils/updateCheck.test.ts +++ b/packages/cli/src/ui/utils/updateCheck.test.ts @@ -649,18 +649,32 @@ describe('classifyUpdateCheckError', () => { ); }); - it.each([ - 'ENOTFOUND', - 'ECONNREFUSED', - 'EAI_AGAIN', - 'ETIMEDOUT', - 'ENETUNREACH', - ])('classifies %s errors as offline', (code) => { - const error = new Error(`request failed`) as NodeJS.ErrnoException; - error.code = code; - expect(classifyUpdateCheckError(error)).toBe('offline'); + it('classifies execFile timeouts as timeout', () => { + const error = Object.assign(new Error('Command failed: npm view'), { + code: null, + killed: true, + signal: 'SIGTERM', + }); + + expect(classifyUpdateCheckError(error)).toBe('timeout'); + }); + + it('classifies ETIMEDOUT errors as timeout', () => { + const error = new Error('request failed') as NodeJS.ErrnoException; + error.code = 'ETIMEDOUT'; + + expect(classifyUpdateCheckError(error)).toBe('timeout'); }); + it.each(['ENOTFOUND', 'ECONNREFUSED', 'EAI_AGAIN', 'ENETUNREACH'])( + 'classifies %s errors as offline', + (code) => { + const error = new Error(`request failed`) as NodeJS.ErrnoException; + error.code = code; + expect(classifyUpdateCheckError(error)).toBe('offline'); + }, + ); + it('classifies network codes embedded in the message as offline', () => { // npm child-process failures surface the code inside stderr text only. expect( @@ -670,6 +684,15 @@ describe('classifyUpdateCheckError', () => { ).toBe('offline'); }); + it('classifies network codes from the error cause as offline', () => { + const cause = new Error('getaddrinfo failed') as NodeJS.ErrnoException; + cause.code = 'ENOTFOUND'; + + expect( + classifyUpdateCheckError(new TypeError('fetch failed', { cause })), + ).toBe('offline'); + }); + it('classifies other errors as registry', () => { expect(classifyUpdateCheckError(new Error('404 Not Found'))).toBe( 'registry', diff --git a/packages/cli/src/ui/utils/updateCheck.ts b/packages/cli/src/ui/utils/updateCheck.ts index 595503ac8de..657b3dad5a9 100644 --- a/packages/cli/src/ui/utils/updateCheck.ts +++ b/packages/cli/src/ui/utils/updateCheck.ts @@ -52,7 +52,6 @@ const NETWORK_ERROR_CODES = [ 'ENOTFOUND', 'ECONNREFUSED', 'EAI_AGAIN', - 'ETIMEDOUT', 'ENETUNREACH', ]; @@ -66,16 +65,27 @@ const NETWORK_ERROR_CODES = [ export function classifyUpdateCheckError( error: unknown, ): UpdateCheckFailureReason { - if (error instanceof UpdateCheckTimeoutError) return 'timeout'; if (error instanceof Error) { - const code = (error as NodeJS.ErrnoException).code; + const errors = [error]; + if (error.cause instanceof Error) errors.push(error.cause); + const matchesCode = (code: string) => + errors.some( + (error) => + (error as NodeJS.ErrnoException).code === code || + error.message.includes(code), + ); + if ( - NETWORK_ERROR_CODES.some( - (netCode) => code === netCode || error.message.includes(netCode), - ) + errors.some((error) => error instanceof UpdateCheckTimeoutError) || + ('killed' in error && + error.killed === true && + 'signal' in error && + error.signal === 'SIGTERM') || + matchesCode('ETIMEDOUT') ) { - return 'offline'; + return 'timeout'; } + if (NETWORK_ERROR_CODES.some(matchesCode)) return 'offline'; } return 'registry'; }