From 2141743b56d6e16ddfcec8781da0d5d462d08f58 Mon Sep 17 00:00:00 2001 From: sungdark Date: Wed, 25 Mar 2026 15:29:14 +0000 Subject: [PATCH 1/2] fix: add timeout and HEAD method to registry validation Fixes https://github.com/asyncapi/cli/issues/2027 - Added AbortController with 10 second timeout to prevent indefinite hangs - Changed from GET to HEAD request for lighter weight validation - Improved error messages to preserve original error and indicate timeout --- src/utils/generate/registry.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/utils/generate/registry.ts b/src/utils/generate/registry.ts index 16fdda2e5..da9e09c06 100644 --- a/src/utils/generate/registry.ts +++ b/src/utils/generate/registry.ts @@ -8,12 +8,22 @@ 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(), 10_000); // 10 second timeout try { - const response = await fetch(registryUrl as string); + const response = await fetch(registryUrl as string, { + method: 'HEAD', + signal: controller.signal, + }); + clearTimeout(timeout); 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 (err: any) { + clearTimeout(timeout); + if (err.name === 'AbortError') { + throw new Error(`Registry URL timed out after 10 seconds: ${registryUrl}`); + } + throw new Error(`Can't fetch registryURL: ${registryUrl} — ${err.message}`); } } From b25fda3410391f5217cc35a678edb991d69dfa41 Mon Sep 17 00:00:00 2001 From: sungdark Date: Sat, 28 Mar 2026 03:11:21 +0000 Subject: [PATCH 2/2] fix(registry): add timeout and use HEAD request in registryValidation Fixes #2027 - CLI hangs indefinitely when --registry-url points to an unreachable host (no timeout handling). Changes: - Add AbortController with 5s timeout to prevent indefinite hangs - Use HEAD request instead of GET for lighter network footprint - Provide meaningful error message on timeout (shows URL and timeout) - Add unit tests for registryURLParser and registryValidation - Test timeout behavior against blackhole IP (10.255.255.1) --- src/utils/generate/registry.ts | 8 +++-- test/unit/utils/registry.test.ts | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 test/unit/utils/registry.test.ts diff --git a/src/utils/generate/registry.ts b/src/utils/generate/registry.ts index da9e09c06..720294a0a 100644 --- a/src/utils/generate/registry.ts +++ b/src/utils/generate/registry.ts @@ -6,10 +6,12 @@ export function registryURLParser(input?: string) { } } +const REGISTRY_TIMEOUT_MS = 5000; + export async function registryValidation(registryUrl?: string, registryAuth?: string, registryToken?: string) { if (!registryUrl) { return; } const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 10_000); // 10 second timeout + const timeout = setTimeout(() => controller.abort(), REGISTRY_TIMEOUT_MS); try { const response = await fetch(registryUrl as string, { method: 'HEAD', @@ -22,8 +24,8 @@ export async function registryValidation(registryUrl?: string, registryAuth?: st } catch (err: any) { clearTimeout(timeout); if (err.name === 'AbortError') { - throw new Error(`Registry URL timed out after 10 seconds: ${registryUrl}`); + throw new Error(`Registry URL timed out after ${REGISTRY_TIMEOUT_MS / 1000}s: ${registryUrl}. The host is unreachable or too slow.`); } - throw new Error(`Can't fetch registryURL: ${registryUrl} — ${err.message}`); + throw new Error(`Can't fetch registryURL: ${registryUrl}`); } } diff --git a/test/unit/utils/registry.test.ts b/test/unit/utils/registry.test.ts new file mode 100644 index 000000000..85185ea76 --- /dev/null +++ b/test/unit/utils/registry.test.ts @@ -0,0 +1,52 @@ +import { expect } from 'chai'; +import { registryURLParser, registryValidation } from '../../../src/utils/generate/registry'; + +describe('registryURLParser()', () => { + it('should return undefined for empty input', () => { + expect(registryURLParser(undefined)).to.equal(undefined); + expect(registryURLParser('')).to.equal(undefined); + }); + + it('should throw for invalid URL without protocol', () => { + expect(() => registryURLParser('not-a-url')).to.throw('Invalid --registry-url flag. The param requires a valid http/https url.'); + expect(() => registryURLParser('ftp://example.com')).to.throw('Invalid --registry-url flag. The param requires a valid http/https url.'); + }); + + it('should accept valid http URL', () => { + expect(() => registryURLParser('http://example.com')).to.not.throw(); + }); + + it('should accept valid https URL', () => { + expect(() => registryURLParser('https://example.com')).to.not.throw(); + }); +}); + +describe('registryValidation()', () => { + it('should return undefined when no registryUrl is provided', async () => { + const result = await registryValidation(undefined, undefined, undefined); + expect(result).to.equal(undefined); + }); + + it('should throw when URL is unreachable (timeout)', async () => { + // 10.255.255.1 is a blackhole IP - will never respond + const blackholeUrl = 'http://10.255.255.1:9999'; + try { + await registryValidation(blackholeUrl, undefined, undefined); + expect.fail('Should have thrown'); + } catch (err: any) { + expect(err.message).to.include('timed out'); + expect(err.message).to.include(blackholeUrl); + } + }); + + it('should throw when URL is unreachable (connection refused)', async () => { + // localhost:9 is unlikely to have anything listening + const url = 'http://localhost:9'; + try { + await registryValidation(url, undefined, undefined); + expect.fail('Should have thrown'); + } catch (err: any) { + expect(err.message).to.include('Can\'t fetch registryURL'); + } + }); +});