From 40cc92dd21fd56fa6aeb952639766261e80136b5 Mon Sep 17 00:00:00 2001 From: FraktalDeFiDAO Date: Wed, 18 Mar 2026 05:33:39 -0500 Subject: [PATCH 1/6] fix: add timeout and HEAD request to registry validation - Add 5 second timeout using AbortController - Use HEAD request instead of GET for lighter check - Properly handle timeout error with meaningful message - Fixes asyncapi/cli#2027 --- src/utils/generate/registry.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/utils/generate/registry.ts b/src/utils/generate/registry.ts index 16fdda2e5..d559986e9 100644 --- a/src/utils/generate/registry.ts +++ b/src/utils/generate/registry.ts @@ -8,12 +8,25 @@ export function registryURLParser(input?: string) { export async function registryValidation(registryUrl?: string, registryAuth?: string, registryToken?: string) { if (!registryUrl) { return; } + const TIMEOUT_MS = 5000; + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); + try { - const response = await fetch(registryUrl as string); + const response = await fetch(registryUrl as string, { + method: 'HEAD', + signal: controller.signal, + }); + clearTimeout(timeoutId); + 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 { + } catch (err: any) { + clearTimeout(timeoutId); + if (err.name === 'AbortError') { + throw new Error(`Registry URL timed out after ${TIMEOUT_MS}ms: ${registryUrl}`); + } throw new Error(`Can't fetch registryURL: ${registryUrl}`); } } From 6db00447ebadc8bafc3eeb4d702f94a3412f3ff0 Mon Sep 17 00:00:00 2001 From: FrkatalDeFiDAO Date: Wed, 18 Mar 2026 08:15:38 -0500 Subject: [PATCH 2/6] fix: include underlying error cause in registry validation Previously, the registry validation would swallow error details, making debugging impossible. Now the underlying cause is included in the error message. Fixes: asyncapi/cli#2013 --- src/utils/generate/registry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/generate/registry.ts b/src/utils/generate/registry.ts index d559986e9..47de8ae81 100644 --- a/src/utils/generate/registry.ts +++ b/src/utils/generate/registry.ts @@ -27,6 +27,7 @@ export async function registryValidation(registryUrl?: string, registryAuth?: st if (err.name === 'AbortError') { throw new Error(`Registry URL timed out after ${TIMEOUT_MS}ms: ${registryUrl}`); } - throw new Error(`Can't fetch registryURL: ${registryUrl}`); + const cause = err.cause ? `\nCaused by: ${err.cause}` : ''; + throw new Error(`Can't fetch registryURL: ${registryUrl}${cause}`); } } From 70514289ed7f1609e43207c59bda044ec8f3d877 Mon Sep 17 00:00:00 2001 From: FrkatalDeFiDAO Date: Wed, 18 Mar 2026 08:39:48 -0500 Subject: [PATCH 3/6] fix: reuse loaded AsyncAPI input to avoid duplicate parsing (issue #2017) --- src/apps/cli/commands/generate/fromTemplate.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/apps/cli/commands/generate/fromTemplate.ts b/src/apps/cli/commands/generate/fromTemplate.ts index 32ed32f97..87fd04b2a 100644 --- a/src/apps/cli/commands/generate/fromTemplate.ts +++ b/src/apps/cli/commands/generate/fromTemplate.ts @@ -1,7 +1,6 @@ import { Args } from '@oclif/core'; import { BaseGeneratorCommand } from '@cli/internal/base/BaseGeneratorCommand'; import { load, Specification } from '@models/SpecificationFile'; -import { ValidationError } from '@errors/validation-error'; import { GeneratorError } from '@errors/generator-error'; import { intro } from '@clack/prompts'; import { inverse } from 'picocolors'; @@ -65,19 +64,7 @@ export default class Template extends BaseGeneratorCommand { const watchTemplate = flags['watch']; const genOption = this.buildGenOption(flags, parsedFlags); - let specification: Specification; - try { - specification = await load(asyncapi); - } catch { - return this.error( - new ValidationError({ - - type: 'invalid-file', - filepath: asyncapi, - }), - { exit: 1 }, - ); - } + const specification = asyncapiInput; const result = await this.generatorService.generate( specification, From aa160159fef0d31f696826193c292668aa2a9efa Mon Sep 17 00:00:00 2001 From: FrkatalDeFiDAO Date: Wed, 18 Mar 2026 08:44:53 -0500 Subject: [PATCH 4/6] fix: skip redundant AsyncAPI examples fetch when already cached (issue #2015) --- scripts/fetch-asyncapi-example.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/scripts/fetch-asyncapi-example.js b/scripts/fetch-asyncapi-example.js index a354b9d6d..2d319daaf 100644 --- a/scripts/fetch-asyncapi-example.js +++ b/scripts/fetch-asyncapi-example.js @@ -24,6 +24,26 @@ const parser = new Parser({ const SPEC_EXAMPLES_ZIP_URL = 'https://github.com/asyncapi/spec/archive/refs/heads/master.zip'; const EXAMPLE_DIRECTORY = path.join(__dirname, '../assets/examples'); const TEMP_ZIP_NAME = 'spec-examples.zip'; +const EXAMPLES_JSON_PATH = path.join(EXAMPLE_DIRECTORY, 'examples.json'); + +const shouldFetchExamples = (force = false) => { + if (force) { + console.log('Force flag detected, fetching examples...'); + return true; + } + if (!fs.existsSync(EXAMPLE_DIRECTORY)) { + return true; + } + if (!fs.existsSync(EXAMPLES_JSON_PATH)) { + return true; + } + const content = fs.readFileSync(EXAMPLES_JSON_PATH, { encoding: 'utf-8' }); + if (!content || content.trim() === '') { + return true; + } + console.log('Examples already exist, skipping fetch. Use --force to refresh.'); + return false; +}; const fetchAsyncAPIExamplesFromExternalURL = () => { try { @@ -119,6 +139,13 @@ const tidyUp = async () => { }; (async () => { + const args = process.argv.slice(2); + const force = args.includes('--force') || args.includes('-f'); + + if (!shouldFetchExamples(force)) { + return; + } + await fetchAsyncAPIExamplesFromExternalURL(); await unzipAsyncAPIExamples(); await buildCLIListFromExamples(); From ae642b7964d83750ff47c82ffe0932476cfeb574 Mon Sep 17 00:00:00 2001 From: FrkatalDeFiDAO Date: Wed, 18 Mar 2026 17:23:05 -0500 Subject: [PATCH 5/6] fix: support reading AsyncAPI documents from stdin (-) Add support for reading AsyncAPI documents from stdin using '-' as the input path. This is a widely accepted convention in Unix-style CLIs. Changes: - Add Specification.fromStdin() method to read from process.stdin - Update load() function to check for '-' and call fromStdin() - Add 'stdin' error type to ErrorLoadingSpec for clear error messages Crypto wallets for bounty: - RTC: RTCbc57f8031699a0bab6e9a8a2769822f19f115dc5 - ETH: 0x742F4fA4224c47C4C4A1d3e4eE4F4e5A2fF8E1 - SOL: FH84Dg6gh7bWtyZ5a1SBNLp1JBesLoCKx9mekJpr7zHR Fixes #2011 --- src/domains/models/SpecificationFile.ts | 34 +++++++++++++++++++++++++ src/errors/specification-file.ts | 6 ++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/domains/models/SpecificationFile.ts b/src/domains/models/SpecificationFile.ts index 5370f6e67..7a55f1ddf 100644 --- a/src/domains/models/SpecificationFile.ts +++ b/src/domains/models/SpecificationFile.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import * as readline from 'readline'; import path from 'path'; import { URL } from 'url'; import yaml from 'js-yaml'; @@ -18,6 +19,7 @@ const allowedFileNames: string[] = [ const TYPE_CONTEXT_NAME = 'context-name'; const TYPE_FILE_PATH = 'file-path'; const TYPE_URL = 'url-path'; +const TYPE_STDIN = 'stdin'; export class Specification { private readonly spec: string; @@ -134,6 +136,33 @@ export class Specification { fileURL: targetUrl, }); } + + static async fromStdin(): Promise { + return new Promise((resolve, reject) => { + const chunks: string[] = []; + const rl = readline.createInterface({ + input: process.stdin, + crlfDelay: Infinity, + }); + + rl.on('line', (line) => { + chunks.push(line); + }); + + rl.on('close', () => { + const spec = chunks.join('\n'); + if (!spec.trim()) { + reject(new Error('No input received from stdin')); + return; + } + resolve(new Specification(spec)); + }); + + rl.on('error', (err) => { + reject(new ErrorLoadingSpec('stdin', '-')); + }); + }); + } } export default class SpecificationFile { @@ -166,6 +195,11 @@ export async function load( // NOSONAR try { if (filePathOrContextName) { + // Handle stdin + if (filePathOrContextName === '-') { + return Specification.fromStdin(); + } + if (loadType?.file) { return Specification.fromFile(filePathOrContextName); } diff --git a/src/errors/specification-file.ts b/src/errors/specification-file.ts index fd64be34d..a4c3c6863 100644 --- a/src/errors/specification-file.ts +++ b/src/errors/specification-file.ts @@ -31,7 +31,7 @@ export class SpecificationURLNotFound extends SpecificationFileError { } } -type From = 'file' | 'url' | 'context' | 'invalid file'; +type From = 'file' | 'url' | 'context' | 'invalid file' | 'stdin'; export class ErrorLoadingSpec extends Error { private readonly errorMessages = { @@ -55,6 +55,10 @@ export class ErrorLoadingSpec extends Error { this.name = 'Invalid AsyncAPI file type'; this.message = 'cli only supports yml ,yaml ,json extension'; } + if (from === 'stdin') { + this.name = 'error loading AsyncAPI document from stdin'; + this.message = 'Reading from stdin (-) is not supported.'; + } if (!from) { this.name = 'error locating AsyncAPI document'; From e3a7becb951533cb008051bef1fffd7b04cc329b Mon Sep 17 00:00:00 2001 From: FrkatalDeFiDAO Date: Wed, 18 Mar 2026 23:14:42 -0500 Subject: [PATCH 6/6] fix: detect and clearly reject multiple YAML documents Fixes issue #1997 - CLI fails with misleading error when AsyncAPI YAML contains multiple documents. The CLI now detects when a YAML file contains multiple documents (separated by ---) and throws a clear, user-friendly error message instead of falling into an incorrect parsing code path. Changes: - Added MultipleYamlDocumentsError class for clear error reporting - Added hasMultipleYamlDocuments() helper function to detect multiple YAML documents - Updated toJson() to check for multi-document YAML before parsing - Updated retrieveFileFormat() to return undefined for multi-document YAML - Added unit test for multi-document YAML detection Closes #1997 --- src/domains/models/SpecificationFile.ts | 32 +++++++++++++++-- src/errors/specification-file.ts | 10 +++++- test/unit/services/validation.service.test.ts | 34 +++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/domains/models/SpecificationFile.ts b/src/domains/models/SpecificationFile.ts index 7a55f1ddf..e1d68ffed 100644 --- a/src/domains/models/SpecificationFile.ts +++ b/src/domains/models/SpecificationFile.ts @@ -4,7 +4,7 @@ import path from 'path'; import { URL } from 'url'; import yaml from 'js-yaml'; import { loadContext } from './Context'; -import { ErrorLoadingSpec } from '@errors/specification-file'; +import { ErrorLoadingSpec, MultipleYamlDocumentsError } from '@errors/specification-file'; import { MissingContextFileError } from '@errors/context-error'; import { fileFormat } from '@cli/internal/flags/format.flags'; import { HttpsProxyAgent } from 'https-proxy-agent'; @@ -21,6 +21,16 @@ const TYPE_FILE_PATH = 'file-path'; const TYPE_URL = 'url-path'; const TYPE_STDIN = 'stdin'; +/** + * Checks if a YAML string contains multiple documents (separated by ---). + * @param content - The YAML content to check + * @returns true if the content contains multiple YAML documents + */ +function hasMultipleYamlDocuments(content: string): boolean { + const yamlDocs = content.split(/^---$/m); + return yamlDocs.length > 1; +} + export class Specification { private readonly spec: string; private readonly filePath?: string; @@ -48,8 +58,20 @@ export class Specification { toJson(): Record { try { - return yaml.load(this.spec, { json: true }) as Record; - } catch { + // Check for multiple YAML documents before parsing + if (hasMultipleYamlDocuments(this.spec)) { + throw new MultipleYamlDocumentsError(); + } + const parsed = yaml.load(this.spec, { json: true }); + // yaml.load can return an array if there are multiple documents with certain YAML configurations + if (Array.isArray(parsed)) { + throw new MultipleYamlDocumentsError(); + } + return parsed as Record; + } catch (err) { + if (err instanceof MultipleYamlDocumentsError) { + throw err; + } return JSON.parse(this.spec); } } @@ -317,6 +339,10 @@ export function retrieveFileFormat(content: string): fileFormat | undefined { JSON.parse(content); return 'json'; } + // Check for multiple YAML documents + if (hasMultipleYamlDocuments(content)) { + return undefined; + } // below yaml.load is not a definitive way to determine if a file is yaml or not. // it is able to load .txt text files also. yaml.load(content); diff --git a/src/errors/specification-file.ts b/src/errors/specification-file.ts index a4c3c6863..68f68f876 100644 --- a/src/errors/specification-file.ts +++ b/src/errors/specification-file.ts @@ -31,7 +31,15 @@ export class SpecificationURLNotFound extends SpecificationFileError { } } -type From = 'file' | 'url' | 'context' | 'invalid file' | 'stdin'; +type From = 'file' | 'url' | 'context' | 'invalid file' | 'stdin' | 'multiple documents'; + +export class MultipleYamlDocumentsError extends SpecificationFileError { + constructor() { + super(); + this.name = 'MultipleYamlDocumentsError'; + this.message = 'AsyncAPI files with multiple YAML documents are not supported. Please provide a single AsyncAPI document.'; + } +} export class ErrorLoadingSpec extends Error { private readonly errorMessages = { diff --git a/test/unit/services/validation.service.test.ts b/test/unit/services/validation.service.test.ts index 401381bfe..b23024322 100644 --- a/test/unit/services/validation.service.test.ts +++ b/test/unit/services/validation.service.test.ts @@ -255,3 +255,37 @@ describe('ValidationService', () => { }); }); }); + +describe('Multi-Document YAML Detection', () => { + let validationService: ValidationService; + + beforeEach(() => { + validationService = new ValidationService(); + }); + + const multiDocumentYAML = `asyncapi: '2.6.0' +info: + title: Test API + version: '1.0.0' +channels: {} + +--- +asyncapi: '2.6.0' +info: + title: Second Doc + version: '1.0.0' +channels: {}`; + + it('should throw error for multiple YAML documents', async () => { + const specFile = new Specification(multiDocumentYAML); + + try { + specFile.toJson(); + // If we get here, the error was not thrown + expect.fail('Expected MultipleYamlDocumentsError to be thrown'); + } catch (err: any) { + expect(err.name).to.equal('MultipleYamlDocumentsError'); + expect(err.message).to.include('multiple YAML documents'); + } + }); +});