diff --git a/.changeset/clever-flowers-dance.md b/.changeset/clever-flowers-dance.md new file mode 100644 index 000000000..085b2ea46 --- /dev/null +++ b/.changeset/clever-flowers-dance.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/cli": patch +--- + +fix: add timeout and error handling to registry URL validation diff --git a/src/utils/generate/registry.ts b/src/utils/generate/registry.ts index 16fdda2e5..6e124ddd8 100644 --- a/src/utils/generate/registry.ts +++ b/src/utils/generate/registry.ts @@ -1,3 +1,5 @@ +const REGISTRY_TIMEOUT_MS = 5000; + export function registryURLParser(input?: string) { if (!input) { return; } const isURL = /^https?:/; @@ -8,12 +10,27 @@ export function registryURLParser(input?: string) { export async function registryValidation(registryUrl?: string, registryAuth?: string, registryToken?: string) { if (!registryUrl) { return; } + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), REGISTRY_TIMEOUT_MS); + try { - const response = await fetch(registryUrl as string); + const response = await fetch(registryUrl as string, { + method: 'HEAD', + signal: controller.signal, + }); if (response.status === 401 && !registryAuth && !registryToken) { throw new Error('You Need to pass either registryAuth in username:password encoded in Base64 or need to pass registryToken'); } - } catch { - throw new Error(`Can't fetch registryURL: ${registryUrl}`); + } catch (error: unknown) { + if (error instanceof Error && error.name === 'AbortError') { + throw new Error(`Registry URL '${registryUrl}' is unreachable (timed out after ${REGISTRY_TIMEOUT_MS / 1000}s). Please verify the URL is correct and accessible.`); + } + if (error instanceof Error && error.message.includes('registryAuth')) { + throw error; + } + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Unable to reach registry URL '${registryUrl}': ${message}`); + } finally { + clearTimeout(timeout); } } diff --git a/test/unit/utils/registry.test.ts b/test/unit/utils/registry.test.ts new file mode 100644 index 000000000..b6da3c4f2 --- /dev/null +++ b/test/unit/utils/registry.test.ts @@ -0,0 +1,38 @@ +import { registryURLParser, registryValidation } from '../../../src/utils/generate/registry'; + +describe('registryURLParser', () => { + it('should return undefined for undefined input', () => { + expect(registryURLParser(undefined)).toBeUndefined(); + }); + + it('should not throw for valid http URL', () => { + expect(() => registryURLParser('http://registry.npmjs.org')).not.toThrow(); + }); + + it('should not throw for valid https URL', () => { + expect(() => registryURLParser('https://registry.npmjs.org')).not.toThrow(); + }); + + it('should throw for non-http URL', () => { + expect(() => registryURLParser('ftp://registry.npmjs.org')).toThrow('Invalid --registry-url flag'); + }); + + it('should throw for plain string', () => { + expect(() => registryURLParser('not-a-url')).toThrow('Invalid --registry-url flag'); + }); +}); + +describe('registryValidation', () => { + it('should return undefined when no registryUrl is provided', async () => { + await expect(registryValidation(undefined)).resolves.toBeUndefined(); + }); + + it('should time out for unreachable hosts', async () => { + // 10.255.255.1 is a non-routable IP that will never respond + await expect(registryValidation('http://10.255.255.1')).rejects.toThrow(/unreachable.*timed out/i); + }, 10000); + + it('should throw a descriptive error for invalid hosts', async () => { + await expect(registryValidation('http://this-host-does-not-exist-at-all.invalid')).rejects.toThrow(/Unable to reach registry URL/); + }); +});