From 85f5224370a81182b3f8abb40dd18a1b5d0d4d97 Mon Sep 17 00:00:00 2001 From: Guciolek Date: Sun, 7 Jun 2026 20:35:35 +0200 Subject: [PATCH 1/2] fix(parser): add throwOnError option to surface validation failures (#878) When parsing an invalid AsyncAPI document, the parser used to silently return a result with 'document: undefined', which made it easy for callers to miss the failure (e.g. 'result.document()' would throw a confusing 'is not a function' error). This change adds a 'throwOnError' option to ParseOptions. When enabled, the parser throws a new 'AsyncAPIParseError' carrying the diagnostics and (when available) the resolved Spectral document whenever the input fails validation with at least one error-severity diagnostic. The default behavior is preserved (no throw) to keep this fully backward compatible. Refs: https://github.com/asyncapi/parser-js/issues/878 --- packages/parser/src/parse.ts | 43 +++++++++++++++- packages/parser/test/parse.spec.ts | 79 ++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index f966a0ff7..677974adb 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -4,11 +4,12 @@ import { applyUniqueIds, customOperations } from './custom-operations'; import { validate } from './validate'; import { copy } from './stringify'; import { createAsyncAPIDocument } from './document'; -import { createDetailedAsyncAPI, mergePatch, setExtension, createUncaghtDiagnostic } from './utils'; +import { createDetailedAsyncAPI, mergePatch, setExtension, createUncaghtDiagnostic, hasErrorDiagnostic } from './utils'; import { xParserSpecParsed, xParserApiVersion } from './constants'; -import type { Spectral, Document, RulesetFunctionContext } from '@stoplight/spectral-core'; +import type { Spectral, RulesetFunctionContext } from '@stoplight/spectral-core'; +import { Document } from '@stoplight/spectral-core'; import type { Parser } from './parser'; import type { ResolverOptions } from './resolver'; import type { ValidateOptions } from './validate'; @@ -27,6 +28,16 @@ export interface ParseOptions { applyTraits?: boolean; parseSchemas?: boolean; validateOptions?: Omit; + /** + * If `true`, the parser will throw an `AsyncAPIParseError` when the input + * document fails validation (i.e. one or more diagnostics with + * `severity === DiagnosticSeverity.Error` are produced). The thrown error + * carries the diagnostics on its `diagnostics` property. + * + * Defaults to `false` to preserve the historical "return undefined document + * alongside diagnostics" behavior. See https://github.com/asyncapi/parser-js/issues/878 + */ + throwOnError?: boolean; __unstable?: { resolver?: Omit; }; @@ -35,6 +46,7 @@ export interface ParseOptions { const defaultOptions: ParseOptions = { applyTraits: true, parseSchemas: true, + throwOnError: false, validateOptions: {}, __unstable: {}, }; @@ -54,6 +66,18 @@ export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input, const { validated, diagnostics, extras } = await validate(parser, spectral, asyncapi, { ...options.validateOptions, source: options.source, __unstable: options.__unstable }); if (validated === undefined) { + // Issue #878: when validation fails the parser used to silently return + // a result with `document: undefined`, which made it easy for callers to + // miss the failure (e.g. `result.document()` would throw a confusing + // "is not a function" error). With `throwOnError` enabled we surface the + // failure with a descriptive error instead. + if (options.throwOnError && hasErrorDiagnostic(diagnostics)) { + throw new AsyncAPIParseError( + 'AsyncAPI document failed validation. See the `diagnostics` property for details.', + diagnostics, + extras?.document + ); + } return { document: undefined, diagnostics, @@ -81,6 +105,21 @@ export async function parse(parser: Parser, spectral: Spectral, asyncapi: Input, extras, }; } catch (err: unknown) { + if (err instanceof AsyncAPIParseError) { + throw err; + } return { document: undefined, diagnostics: createUncaghtDiagnostic(err, 'Error thrown during AsyncAPI document parsing', spectralDocument), extras: undefined }; } } + +export class AsyncAPIParseError extends Error { + public readonly diagnostics: Diagnostic[]; + public readonly document?: Document; + + constructor(message: string, diagnostics: Diagnostic[], document?: Document) { + super(message); + this.name = 'AsyncAPIParseError'; + this.diagnostics = diagnostics; + this.document = document; + } +} diff --git a/packages/parser/test/parse.spec.ts b/packages/parser/test/parse.spec.ts index 8717011ee..d7a26077d 100644 --- a/packages/parser/test/parse.spec.ts +++ b/packages/parser/test/parse.spec.ts @@ -1276,3 +1276,82 @@ components: expect(filterLastVersionDiagnostics(diagnostics)[0].range.start.line === 236).toEqual(true); }); }); + +describe('parse() - throwOnError (issue #878)', function () { + const parser = new Parser(); + + const invalidV3Document = { + asyncapi: '3.0.0', + // `test` is not part of the AsyncAPI 3.0 root schema, so this triggers + // a validation error (severity === 0). See https://github.com/asyncapi/parser-js/issues/878 + test: 'hello', + info: { + title: 'Invalid AsyncApi document', + version: '1.0', + }, + channels: {}, + }; + + it('should return document: undefined with throwOnError: false (default behavior)', async function () { + const result = await parser.parse(invalidV3Document); + expect(result.document).toBeUndefined(); + expect(result.diagnostics.some(d => d.severity === 0)).toBe(true); + }); + + it('should return document: undefined when throwOnError: false is passed explicitly', async function () { + const result = await parser.parse(invalidV3Document, { throwOnError: false }); + expect(result.document).toBeUndefined(); + expect(result.diagnostics.some(d => d.severity === 0)).toBe(true); + }); + + it('should throw AsyncAPIParseError when throwOnError: true and there are validation errors', async function () { + await expect(parser.parse(invalidV3Document, { throwOnError: true })) + .rejects.toMatchObject({ + name: 'AsyncAPIParseError', + message: expect.stringContaining('failed validation'), + diagnostics: expect.arrayContaining([ + expect.objectContaining({ severity: 0 }) + ]) + }); + }); + + it('should expose the diagnostics on the thrown error', async function () { + let caught: any; + try { + await parser.parse(invalidV3Document, { throwOnError: true }); + } catch (e) { + caught = e; + } + expect(caught).toBeDefined(); + expect(caught.name).toBe('AsyncAPIParseError'); + expect(Array.isArray(caught.diagnostics)).toBe(true); + expect(caught.diagnostics.some((d: any) => d.severity === 0)).toBe(true); + }); + + it('should not throw on a valid document even with throwOnError: true', async function () { + const validDoc = { + asyncapi: '3.0.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0', + }, + channels: {}, + }; + const result = await parser.parse(validDoc, { throwOnError: true }); + expect(result.document).toBeInstanceOf(AsyncAPIDocumentV3); + }); + + it('should not throw when diagnostics only contain warnings/infos and throwOnError: true', async function () { + // latest-version info + no errors + const minorDoc = { + asyncapi: '3.0.0', + info: { + title: 'Valid but old version', + version: '1.0', + }, + channels: {}, + }; + const result = await parser.parse(minorDoc, { throwOnError: true }); + expect(result.document).toBeDefined(); + }); +}); From 990fa1fe99333fe49ec28f1d65e2d01c8087883b Mon Sep 17 00:00:00 2001 From: Nabu Date: Tue, 9 Jun 2026 06:52:17 +0200 Subject: [PATCH 2/2] chore(changeset): add changeset for #1182 throwOnError --- .changeset/throw-on-validation-error.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/throw-on-validation-error.md diff --git a/.changeset/throw-on-validation-error.md b/.changeset/throw-on-validation-error.md new file mode 100644 index 000000000..ccec5bf18 --- /dev/null +++ b/.changeset/throw-on-validation-error.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/parser": minor +--- + +Add `throwOnError` option to `parse()` to surface validation errors as thrown exceptions instead of returning them in the result. Defaults to `false` to preserve backward compatibility. When set to `true`, validation errors are thrown synchronously from `parse()`, and `parseAndValidate()` rethrows the same error rather than just attaching it to the result.