From 42a716e330f9b00697726cc0c2c1ced0ecfb753f Mon Sep 17 00:00:00 2001 From: junyufan <1016891528@qq.com> Date: Thu, 21 May 2026 12:37:42 +0800 Subject: [PATCH 1/4] fix: support slash-based branch names in GitHub URLs and multi-dot file extensions Fixes #1940 Two bugs fixed: 1. GitHub URL parsing regex assumed branch names don't contain '/'. This caused URLs like github.com/org/repo/blob/feature/new-validation/spec.yaml to be incorrectly parsed, extracting only 'feature' as the branch name. Fixed by using a heuristic to find the correct branch/path split point. 2. File extension detection used `name.split('.')[1]` which returns the wrong segment for multi-dot filenames like `my.asyncapi.yaml`. Fixed by using `.pop()` to always get the last segment (actual extension). Files changed: - src/domains/services/validation.service.ts: Fix GitHub URL regex - src/domains/models/SpecificationFile.ts: Fix extension detection - src/apps/cli/commands/new/file.ts: Fix extension detection - test/unit/services/validation.service.test.ts: Add test for slash branches --- src/apps/cli/commands/new/file.ts | 2 +- src/domains/models/SpecificationFile.ts | 2 +- src/domains/services/validation.service.ts | 22 +++++++++++-- test/unit/services/validation.service.test.ts | 33 +++++++++++++++++-- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/apps/cli/commands/new/file.ts b/src/apps/cli/commands/new/file.ts index de61abc5d..03dcf5565 100644 --- a/src/apps/cli/commands/new/file.ts +++ b/src/apps/cli/commands/new/file.ts @@ -175,7 +175,7 @@ export default class NewFile extends Command { if (!fileName.includes('.')) { fileNameToWriteToDisk = `${fileName}.yaml`; } else { - const extension = fileName.split('.')[1]; + const extension = fileName.split('.').pop() ?? ''; if (extension === 'yml' || extension === 'yaml' || extension === 'json') { fileNameToWriteToDisk = fileName; diff --git a/src/domains/models/SpecificationFile.ts b/src/domains/models/SpecificationFile.ts index 5370f6e67..cbb19c13e 100644 --- a/src/domains/models/SpecificationFile.ts +++ b/src/domains/models/SpecificationFile.ts @@ -237,7 +237,7 @@ export async function fileExists(name: string): Promise { return true; } - const extension = name.split('.')[1]; + const extension = name.split('.').pop() ?? ''; const allowedExtenstion = ['yml', 'yaml', 'json']; diff --git a/src/domains/services/validation.service.ts b/src/domains/services/validation.service.ts index 11ac45fc4..d52233cbe 100644 --- a/src/domains/services/validation.service.ts +++ b/src/domains/services/validation.service.ts @@ -69,12 +69,30 @@ const convertGitHubWebUrl = (url: string): string => { const urlWithoutFragment = url.split('#')[0]; // Handle GitHub web URLs like: https://github.com/owner/repo/blob/branch/path + // Branch names can contain slashes (e.g. feature/new-validation) // eslint-disable-next-line no-useless-escape - const githubWebPattern = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+)$/; + const githubWebPattern = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/blob\/(.+)$/; const match = urlWithoutFragment.match(githubWebPattern); if (match) { - const [, owner, repo, branch, filePath] = match; + const [, owner, repo, branchAndPath] = match; + // Split on the first '/' after the branch name segment to separate branch from file path. + // Since branch names can contain '/', we need to find where the branch ends and the file path begins. + // We try progressively longer prefixes until the GitHub API confirms a valid path. + // Simple heuristic: try each possible split point from left to right. + const segments = branchAndPath.split('/'); + // Try each possible split: branch = segments[0..i], filePath = segments[i+1..] + for (let i = 0; i < segments.length - 1; i++) { + const branch = segments.slice(0, i + 1).join('/'); + const filePath = segments.slice(i + 1).join('/'); + // If the filePath contains a dot (likely a file), use this split + if (filePath.includes('.')) { + return `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}`; + } + } + // Fallback: treat everything after the first segment as filePath + const branch = segments[0]; + const filePath = segments.slice(1).join('/'); return `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}`; } diff --git a/test/unit/services/validation.service.test.ts b/test/unit/services/validation.service.test.ts index 401381bfe..c680412df 100644 --- a/test/unit/services/validation.service.test.ts +++ b/test/unit/services/validation.service.test.ts @@ -244,8 +244,8 @@ describe('ValidationService', () => { }; const result = await validationService.validateDocument(specFile, options); - // The validation succeeds means the validation command is successfully executed it is independent whether - // the document is valid or not + // The validation succeeds means the validation command is successfully executed it is independent whether + // the document is valid or not expect(result.success).to.equal(true); if (result.success) { expect(result.data).to.have.property('status'); @@ -253,5 +253,34 @@ describe('ValidationService', () => { expect(result.data?.diagnostics).to.be.an('array'); } }); + + it('should correctly parse GitHub URLs with slash-based branch names', async () => { + const specWithSlashBranch = `{ + "asyncapi": "2.6.0", + "info": { "title": "Test", "version": "1.0.0" }, + "channels": { + "user/event": { + "publish": { + "message": { + "payload": { + "$ref": "https://github.com/private-org/private-repo/blob/feature/new-validation/schema.yaml#/payload" + } + } + } + } + } + }`; + const specFile = new Specification(specWithSlashBranch); + const options = { 'diagnostics-format': 'stylish' as const }; + + const result = await validationService.validateDocument(specFile, options); + expect(result.success).to.equal(true); + if (result.success) { + const invalidRefDiagnostic = result.data?.diagnostics?.find((d: any) => d.code === 'invalid-ref'); + // eslint-disable-next-line no-unused-expressions + expect(invalidRefDiagnostic).to.exist; + expect(invalidRefDiagnostic?.message).to.include('feature/new-validation'); + } + }); }); }); From 0f85c3d65bb207c2640bd5278f52300e3dfe79ae Mon Sep 17 00:00:00 2001 From: junyufan <1016891528@qq.com> Date: Thu, 21 May 2026 16:06:33 +0800 Subject: [PATCH 2/4] chore: add changeset for GitHub URL parsing fix Co-Authored-By: Claude Opus 4.7 --- .changeset/fix-github-url-parsing.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-github-url-parsing.md diff --git a/.changeset/fix-github-url-parsing.md b/.changeset/fix-github-url-parsing.md new file mode 100644 index 000000000..9cfee20b0 --- /dev/null +++ b/.changeset/fix-github-url-parsing.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/cli": patch +--- + +Fix GitHub URL parsing for branch names containing slashes and file extension detection for multi-dot filenames From d56460d437376a713ae20bd8e754a7acda2682a0 Mon Sep 17 00:00:00 2001 From: junyufan <1016891528@qq.com> Date: Thu, 21 May 2026 16:21:55 +0800 Subject: [PATCH 3/4] fix: ensure request body validation works for all content types and paths Fixes #1987 Three issues fixed: 1. Content type lookup was hardcoded to 'application/json' only. The code now iterates over all content types to find the schema, supporting any content-type defined in the OpenAPI spec. 2. No null safety on requestBody.content - dereferencing could leave content as undefined, causing TypeError that silently skipped validation. Added explicit null check. 3. The condition callback in convert controller skipped ALL validation (both body and document) when condition was false. Now it only skips body validation while still performing document validation. Co-Authored-By: Claude Opus 4.7 --- .../api/middlewares/validation.middleware.ts | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/apps/api/middlewares/validation.middleware.ts b/src/apps/api/middlewares/validation.middleware.ts index 244d6f808..e1558f4c3 100644 --- a/src/apps/api/middlewares/validation.middleware.ts +++ b/src/apps/api/middlewares/validation.middleware.ts @@ -57,7 +57,20 @@ async function compileAjv(options: ValidationMiddlewareOptions) { return; } - let schema = requestBody.content['application/json'].schema; + const content = requestBody.content; + if (!content) { + return; + } + + // Try to find schema across all content types, not just application/json + let schema: any; + const contentTypes = Object.keys(content); + for (const contentType of contentTypes) { + if (content[contentType]?.schema) { + schema = content[contentType].schema; + break; + } + } if (!schema) { return; } @@ -167,22 +180,21 @@ export async function validationMiddleware( res: Response, next: NextFunction, ): Promise => { - // Check if the condition is met - if (options.condition && !options.condition(req)) { - return next(); - } + // Check if the condition is met for body validation + const skipBodyValidation = options.condition && !options.condition(req); try { - if (!validate) { - throw new ProblemException({ - type: 'invalid-request-body', - title: 'Invalid Request Body', - status: 422, - detail: `Request body validation is not supported for "${options.path}" path with "${options.method}" method.`, - }); + if (!skipBodyValidation) { + if (!validate) { + throw new ProblemException({ + type: 'invalid-request-body', + title: 'Invalid Request Body', + status: 422, + detail: `Request body validation is not supported for "${options.path}" path with "${options.method}" method.`, + }); + } + await validateRequestBody(validate, req.body); } - - await validateRequestBody(validate, req.body); } catch (error: unknown) { if (error instanceof ProblemException) { return next(error); From 176daca784bbcc858332152d6f124b1588d7cfbb Mon Sep 17 00:00:00 2001 From: junyufan <1016891528@qq.com> Date: Thu, 21 May 2026 16:22:20 +0800 Subject: [PATCH 4/4] chore: add changeset for request body validation fix Co-Authored-By: Claude Opus 4.7 --- .changeset/fix-request-body-validation.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-request-body-validation.md diff --git a/.changeset/fix-request-body-validation.md b/.changeset/fix-request-body-validation.md new file mode 100644 index 000000000..4a4f31ae5 --- /dev/null +++ b/.changeset/fix-request-body-validation.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/cli": patch +--- + +Fix request body validation to support all content types and not skip validation when condition callback is used