fix(extensions): accept uppercase marketplace source schemes - #5435
Conversation
✅ Local real-build verification — LGTMI built this PR at its head ( Verdict: the fix is correct, the new tests genuinely guard it, there is no regression, and it incidentally closes a latent crash. Safe to merge. 1. Mutation test — the 7 new tests are non-vacuousReverted only
On the fixed source all 92 pass (matches the PR's claim). 2. Built-
|
| Source | Pre-fix | Fixed |
|---|---|---|
https://github.com/owner/repo |
github |
github |
HTTPS://github.com/owner/repo |
local ❌ |
github ✅ |
Https://GitHub.com/owner/repo |
local ❌ |
github ✅ |
HTTPS://example.com/marketplace.json |
local ❌ |
http ✅ |
HTTP://example.com/marketplace.json |
local ❌ |
http ✅ |
GIT@github.com:owner/repo.git |
local ❌ |
git ✅ |
SSO://team/repo |
local ❌ |
git ✅ |
lowercase https/http/git@/sso + owner/repo + ./local |
correct | correct |
loadMarketplaceConfigFromSource against a real local HTTP server (127.0.0.1, served valid marketplace.json):
| Source | Pre-fix | Fixed |
|---|---|---|
http://127.0.0.1:PORT/marketplace.json |
throws ERR_INVALID_PROTOCOL "Protocol http: not supported. Expected https:" ❌ |
loaded ✅ |
HTTP://127.0.0.1:PORT/marketplace.json |
null (Priority-2 skipped) ❌ |
loaded ✅ |
The server logged the real http.get hits on the fixed run, confirming the protocol routing actually takes effect.
3. Real-binary A/B in tmux — qwen extensions sources
Same cli/dist/index.js, isolated HOME, against the local server:
| Command | Pre-fix | Fixed |
|---|---|---|
sources add HTTP://…/marketplace.json (uppercase) |
✗ exit 1 — "looks like a single extension, not a marketplace" | ✓ Added marketplace "mp-uppercase" — Type: http |
sources add http://…/marketplace.json (http routing) |
✗ exit 1 — "Protocol http: not supported. Expected https:" | ✓ Added marketplace "mp-lowercase" — Type: http |
sources list |
No marketplace sources added yet. |
both listed, Type: http |
tmux pane transcript (fixed)
===== [FIXED] add UPPERCASE scheme: HTTP://127.0.0.1:38573/marketplace.json?n=mp-uppercase =====
Added marketplace "mp-uppercase".
EXIT=0
===== [FIXED] add lowercase http (http-vs-https routing): http://127.0.0.1:38573/marketplace.json?n=mp-lowercase =====
Added marketplace "mp-lowercase".
EXIT=0
===== [FIXED] sources list =====
mp-uppercase
Source: HTTP://127.0.0.1:38573/marketplace.json?n=mp-uppercase (Type: http)
mp-lowercase
Source: http://127.0.0.1:38573/marketplace.json?n=mp-lowercase (Type: http)
EXIT=0
4. Bonus: the PR also closes a latent timer crash 🎁
When https.get threw synchronously on an http:// URL, the pre-fix fetchUrl left its 10s hardDeadline timer armed (never cleared), and the timer body referenced the const req that was never initialized. ~10s later it crashes the process:
ReferenceError: Cannot access 'req' before initialization
at Timeout._onTimeout (…/marketplace.js:115:13)
This is masked in the one-shot CLI by the immediate process.exit(1), but a long-lived flow (Discover → discoverPlugins) would hit the uncaught timer. The PR's rework (let req: ClientRequest | undefined, req?.destroy(), try/catch around client.get → done(null)) eliminates it — the fixed harness exits cleanly.
5. Minor, non-blocking notes
http://marketplace sources now genuinely fetch over plaintext HTTP. This is the intended behavior and only when the user explicitly supplies anhttp://URL; the body-size cap and wall-clock timeout guards are preserved through the refactor.- The
git@/sso://uppercase paths (lowerTrimmed.startsWith('git@'), regex/i) and theclaude-converterHTTP://(vs the testedHTTPS://) variant are covered by the fix but have no dedicated unit row — both verified green here, just a small coverage suggestion for a follow-up.
🇨🇳 中文版(完整对应)
✅ 本地真实构建验证 —— 可以合并
我在隔离 worktree 中以 PR 最新提交(dc9e9380f,base 61dcf865de)完整构建(npm ci && npm run build),并从三个相互独立的层面做了验证:变异测试、针对真实本地 HTTP marketplace 服务器的构建产物函数 A/B、以及 tmux 中真实 qwen extensions sources 二进制 A/B。pre-fix 与 fixed 的对比是通过「只回退 3 个生产文件并重新构建 @qwen-code/qwen-code-core」得到的(CLI 在运行时解析 core/dist,所以同一个 cli/dist 二进制会自动加载各自的变体)。环境:macOS,Node v22.22.2。
结论:修复正确,新增测试确实能守住该修复,无回归,并且顺带消除了一个潜在崩溃。可以合并。
1. 变异测试 —— 7 个新测试非空过
只把 sourceRegistry.ts / marketplace.ts / claude-converter.ts 回退到 base,保留 PR 的测试,跑 3 个测试套件:
Tests 7 failed | 85 passed (92)
sourceRegistry.test.ts—— 3 个新增的大写用例失败,所有小写用例仍通过 → 该 bug 只影响大写。marketplace.test.ts—— 3 个新增的大写 loader 测试失败。claude-converter.test.ts—— 失败信息是铁证:Plugin source not found at …/plugin-uppercase-url/HTTPS:/github.com/owner/plugin→ pre-fix 把HTTPS://…当成了本地路径。
在修复后的源码上 92 个全部通过(与 PR 描述一致)。
2. 构建产物函数 A/B(真实请求,无 mock)
parseExtensionSourceType(纯函数):
| 源串 | Pre-fix | Fixed |
|---|---|---|
https://github.com/owner/repo |
github |
github |
HTTPS://github.com/owner/repo |
local ❌ |
github ✅ |
Https://GitHub.com/owner/repo |
local ❌ |
github ✅ |
HTTPS://example.com/marketplace.json |
local ❌ |
http ✅ |
HTTP://example.com/marketplace.json |
local ❌ |
http ✅ |
GIT@github.com:owner/repo.git |
local ❌ |
git ✅ |
SSO://team/repo |
local ❌ |
git ✅ |
小写 https/http/git@/sso + owner/repo + ./local |
正确 | 正确 |
loadMarketplaceConfigFromSource 针对真实本地 HTTP 服务器(127.0.0.1,返回合法 marketplace.json):
| 源串 | Pre-fix | Fixed |
|---|---|---|
http://127.0.0.1:PORT/marketplace.json |
抛 ERR_INVALID_PROTOCOL「Protocol http: not supported. Expected https:」 ❌ |
成功加载 ✅ |
HTTP://127.0.0.1:PORT/marketplace.json |
null(跳过 Priority-2) ❌ |
成功加载 ✅ |
fixed 这一侧服务器记录到真实的 http.get 请求,证明协议路由确实生效。
3. tmux 中真实二进制 A/B —— qwen extensions sources
同一个 cli/dist/index.js,隔离 HOME,对本地服务器:
| 命令 | Pre-fix | Fixed |
|---|---|---|
sources add HTTP://…/marketplace.json(大写) |
✗ 退出码 1 —— "looks like a single extension, not a marketplace" | ✓ Added marketplace "mp-uppercase" —— Type: http |
sources add http://…/marketplace.json(http 路由) |
✗ 退出码 1 —— "Protocol http: not supported. Expected https:" | ✓ Added marketplace "mp-lowercase" —— Type: http |
sources list |
No marketplace sources added yet. |
两条都在,Type: http |
4. 额外收获:本 PR 还顺带修掉一个潜在的定时器崩溃 🎁
当 https.get 在遇到 http:// URL 时同步抛错,pre-fix 的 fetchUrl 会让那个 10s 的 hardDeadline 定时器一直挂着(从未 clear),而定时器体里引用了那个从未被初始化的 const req。约 10s 后进程崩溃:
ReferenceError: Cannot access 'req' before initialization
at Timeout._onTimeout (…/marketplace.js:115:13)
在一次性 CLI 里,它被紧随其后的 process.exit(1) 掩盖了;但长期存活的流程(Discover → discoverPlugins)会真正触发这个未捕获的定时器异常。PR 的改写(let req: ClientRequest | undefined、req?.destroy()、在 client.get 外包 try/catch 并 done(null))消除了它 —— fixed 的 harness 干净退出。
5. 非阻塞的小提示
http://marketplace 源现在会真正走明文 HTTP 请求。这是预期行为,且仅当用户显式提供http://URL 时发生;重构过程中保留了响应体大小上限与墙钟超时这两个保护。- 大写
git@/sso://路径(lowerTrimmed.startsWith('git@')、正则/i)以及claude-converter的HTTP://(相对已测的HTTPS://)变体都被修复覆盖,但没有专门的单测用例 —— 这里都已验证为绿,仅作为后续补测的小建议。
Verified locally by a maintainer on a real build (worktree npm ci && npm run build); pre-fix/fixed via source revert + core rebuild. Not an automated CI run.
wenshao
left a comment
There was a problem hiding this comment.
The case-insensitive scheme handling is correct and consistent across claude-converter.ts / marketplace.ts / sourceRegistry.ts; the fetchUrl http/https dispatch is sound (timers cleaned up, req? null-safe, no regression vs the https-only version — it even removes a latent TDZ); and the new tests are non-vacuous (mutation-checked: each fails when its fix is reverted). Lint, types, and the 3 affected suites (92 tests) pass locally; CI green.
Two non-blocking suggestions inline. Verdict: Comment.
中文版
大小写不敏感的 scheme 处理在 claude-converter.ts / marketplace.ts / sourceRegistry.ts 三处都正确且一致;fetchUrl 的 http/https 分发也是稳的(定时器有清理、req? 空安全、相比只支持 https 的旧版无回归,还顺手消除了一个潜在的 TDZ);新增测试非空过(做了变异测试:回退对应修复时各自会失败)。本地 lint、类型、3 个相关测试套件(92 个用例)均通过;CI 全绿。
两条非阻塞建议见行内评论。结论:Comment。
— claude-opus-4-8 via Claude Code /qreview
| try { | ||
| const protocol = new URL(url).protocol.toLowerCase(); | ||
| if (protocol === 'http:') { | ||
| client = http; |
There was a problem hiding this comment.
[Suggestion] Nice — this makes plaintext http:// marketplace fetches actually work (previously fetchUrl was https-only, so an http:// source failed at the TLS layer; this closes the deferred follow-up from #4850). The refactor itself is solid: hardDeadline is still cleared in done(), req?.destroy() is null-safe, and the client.get try/catch handles a synchronous throw.
One security consideration now that it's functional: marketplace.json drives the pre-consent Discover list and the per-plugin installSource that flows into cloneFromGit / downloadFromGitHubRelease. Fetched over plaintext http://, an on-path attacker can rewrite that body and redirect an install to an attacker-controlled repo. (http.get doesn't follow redirects, so this is metadata-MITM, not redirect-SSRF; and it's bounded — the source is user-added and install still requires consent.) Worth considering a guard: restrict http:// to loopback / RFC-1918 hosts, or a one-line "insecure http source" warning at add time, so a network MITM can't silently hijack install routing. Likely a maintainer call — not a blocker.
中文
这次让明文 http:// 的 marketplace 拉取真正生效了(之前 fetchUrl 只支持 https,http:// 源会在 TLS 层失败;补上了 #4850 里延后的后续项)。重构本身没问题:done() 仍会清掉 hardDeadline,req?.destroy() 是空安全的,client.get 也用 try/catch 兜住了同步抛错。
既然现在能用了,有个安全考量:marketplace.json 既驱动安装前的 Discover 列表,也提供流向 cloneFromGit / downloadFromGitHubRelease 的每个插件 installSource。在明文 http:// 上,链路中间人可以改写响应体,把安装重定向到攻击者仓库。(http.get 默认不跟随重定向,所以是元数据 MITM 而非重定向 SSRF;且有边界——源由用户手动添加,安装仍需确认。)建议加个限制:把 http:// 限定在 loopback / 内网地址,或在添加源时给一行"不安全的 http 源"警告,避免网络 MITM 静默劫持安装路由。更像维护者的取舍——不阻塞。
— claude-opus-4-8 via Claude Code /qreview
| ['sso://team/repo', 'git'], | ||
| ['https://example.com/marketplace.json', 'http'], | ||
| ['HTTPS://example.com/marketplace.json', 'http'], | ||
| ['HTTP://example.com/marketplace.json', 'http'], |
There was a problem hiding this comment.
[Suggestion] This table now covers uppercase HTTPS:// / HTTP:// well, but parseExtensionSourceType also made the git@ / sso:// branch case-insensitive (lower.startsWith('git@') || lower.startsWith('sso://')) and that half is only tested in lowercase (rows 33-34). Reverting just that branch to trimmed.startsWith(...) leaves every test green — so the uppercase git@/sso:// classification is unguarded. Two rows close it:
| ['HTTP://example.com/marketplace.json', 'http'], | |
| ['HTTP://example.com/marketplace.json', 'http'], | |
| ['GIT@github.com:owner/repo.git', 'git'], | |
| ['SSO://team/repo', 'git'], |
中文
这个表对大写 HTTPS:// / HTTP:// 覆盖得很好,但 parseExtensionSourceType 同样把 git@ / sso:// 分支改成了大小写不敏感(lower.startsWith('git@') || lower.startsWith('sso://')),而这一半只测了小写(第 33-34 行)。把该分支退回 trimmed.startsWith(...) 时所有测试仍全绿——说明大写 git@/sso:// 的分类没有被测试守住。补两行即可。
— claude-opus-4-8 via Claude Code /qreview
|
@qwen-code /triage |
|
Thanks for the PR! Template looks good ✓ — all required sections present, bilingual, reviewer test plan included. On direction: this is a straightforward RFC 3986 compliance fix — URL schemes are defined as case-insensitive, and the marketplace source paths were still doing case-sensitive prefix checks. It's the direct follow-up to #5429 (install sources) covering the remaining marketplace code paths. Clearly aligned with the project's extension system, no safety or public-contract concerns. On approach: the scope is tight and focused. Three production files, each applying the same minimal change — lowercase the prefix for comparison, preserve the original string for parsing/fetching. The Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ — 所有必需章节齐全,双语,包含 reviewer test plan。 方向:这是一个直截了当的 RFC 3986 合规修复 — URL scheme 按规范是大小写不敏感的,而 marketplace source 路径仍在做大小写敏感的前缀检查。它是 #5429(install source)的直接后续,覆盖了 marketplace 代码中剩余的路径。与项目的 extension 系统完全对齐,无安全或公共接口方面的问题。 方案:范围紧凑且聚焦。三个生产文件,每个都做同样的最小改动 — 对前缀做 lowercase 比较,保留原始字符串用于解析和请求。 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading the diff): the fix is mechanical — lowercase the input (or just the scheme prefix) before comparing against Comparison with the diff: the PR's approach matches my independent proposal exactly. It lowercases only the prefix for comparison while preserving the original string — the correct pattern since One observation: VerificationUnit Tests (PR branch)Mutation TestReverted only the 3 production files ( The 6 failures confirm the new tests genuinely guard the fix — Typecheck & LintBefore/After Probe —
|
|
Stepping back: this is a clean, well-scoped compliance fix. URL schemes are case-insensitive per RFC 3986, the marketplace source paths weren't honoring that, and the fix applies the minimum change needed — lowercase prefix comparison in three functions, protocol-aware client selection via a shared The implementation matches my independent proposal. It preserves the original source string for parsing/fetching (correct — The before/after probe and mutation test both confirm the fix works and the tests are non-vacuous. All existing lowercase inputs behave identically — zero regression surface. Typecheck and lint are clean. Note on the maintainer's DRY concern about folding in #5437: this PR already does the DRY work by extracting LGTM. Approving. ✅ 中文说明退一步看:这是一个干净、范围合理的合规修复。URL scheme 按 RFC 3986 是大小写不敏感的,marketplace source 路径没有遵守这一点,修复应用了所需的最小改动 — 三个函数中的小写前缀比较、通过共享 实现与我的独立提案一致。保留原始 source 字符串用于解析和请求(正确 — Before/After 探测和变异测试都确认了修复有效且测试非空过。所有现有的小写输入行为不变 — 零回归面。Typecheck 和 lint 全部通过。 关于维护者提出的将 #5437 合并的 DRY 建议:本 PR 已经通过将 可以合并。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
📌 Same maintainer note — full version on #5509. This is the case-insensitive URL-scheme series. #5437 is the same idea in |
dc9e938 to
60a974b
Compare
|
@qwen-code /triage |
1 similar comment
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
✅ Local verification report — uppercase marketplace source schemes (#5435)Verified locally in Environment
1. The PR's stated test plan — reproduced
¹ The build fails on 2. Revert-proof — the new tests are coupled to the fixReverting the source files to base (removing
All pass on the merged code. 3. Adversarial + real-server verification (19/19)Drove the real exported helpers; for the loader I stood up a real
Real marketplace load over the network:
Notes (non-blocking)
VerdictApprove. The fix correctly treats URL schemes as case-insensitive (compare lowercased, fetch/parse/store the original), consolidates HTTP-client selection into one tested helper, preserves all lowercase behavior, and is confirmed end-to-end against a real server. The only red — 🇨🇳 中文版验证报告(点击展开)✅ 本地验证报告 —— marketplace source 大写 scheme(#5435)在 环境
1. PR 自述测试计划 —— 复现
¹ build 失败在 2. Revert-proof —— 新测试与修复强绑定把源文件回退到 base(删除
在合并后的代码上全部通过。 3. 对抗性 + 真实 server 验证(19/19)驱动真实导出的 helper;loader 部分启动了真实的 localhost
真实网络 marketplace 加载:
提示(非阻塞)
结论赞成合并。 修复正确地把 URL scheme 当作大小写不敏感(比较时转小写,fetch/解析/存储用原始串),把 HTTP client 选择整合进一个有测试的 helper,保留了所有小写行为,并已用真实 server 端到端确认。唯一的红 —— Verification method: merged worktree; PR test plan reproduced (120/120 + typecheck + lint); revert-proof (6 uppercase tests fail on base); adversarial battery of |
What this PR does
Accepts uppercase
HTTP:///HTTPS://schemes in the extension marketplace paths that were still doing case-sensitive prefix checks. The source registry, marketplace config loader, and Claude marketplace plugin string-source resolver now lowercase only for scheme comparison while preserving the original source string for parsing, fetching, and install metadata.It also makes marketplace direct-JSON fetches reuse the shared
clientForUrl()helper introduced for npm registry URLs, so marketplace and npm extension downloads now choosehttp.getvshttps.getfrom one parsed-URL protocol helper instead of carrying duplicate client-selection logic.Why it's needed
URL schemes are case-insensitive. #5429 fixed this for extension install sources, but marketplace sources still misclassified
HTTPS://...as local and skipped the remote loading path. The same gap existed for stringsourceentries inside Claude marketplace plugins.Reviewer Test Plan
How to verify
Run the focused extension tests and core validation commands below. The important regression cases are uppercase GitHub marketplace sources, uppercase direct JSON marketplace sources, uppercase HTTP direct JSON marketplace sources, uppercase marketplace plugin string sources, and uppercase npm registry/tarball URLs continuing to use the shared helper.
Evidence (Before & After)
Before: the new uppercase scheme tests failed because
HTTPS://...was classified as local or skipped the HTTP(S) loader.HTTP://...could also enter a remote path that still calledhttps.get, and npm/marketplace carried separate protocol-to-client helpers.After:
npx vitest run packages/core/src/extension/npm.test.ts packages/core/src/extension/marketplace.test.ts packages/core/src/extension/sourceRegistry.test.ts packages/core/src/extension/claude-converter.test.tspasses with 120 tests.Additional local checks passed:
Tested on
Environment (optional)
Node.js v26.3.0, npm 11.16.0.
Risk & Scope
http://direct marketplace JSON sources now use the HTTP client instead of accidentally going through the HTTPS client path; npm and marketplace now share one URL-client helper./extensionsTUI testing; this is covered at the core parser/loader level.Linked Issues
Fixes #5434
中文说明
这个 PR 做了什么
让 extension marketplace 相关路径接受大写
HTTP:///HTTPS://scheme。source registry、marketplace config loader、Claude marketplace 插件里的字符串source解析,现在只在比较 scheme 时做 lowercase,后续解析、请求和安装 metadata 仍保留用户输入的原始字符串。同时,marketplace direct-JSON fetch 复用 npm registry URL 已引入的共享
clientForUrl()helper,让 marketplace 和 npm extension download 都通过同一个 parsed-URL protocol helper 选择http.get或https.get,避免重复实现 client-selection 逻辑。为什么需要
URL scheme 按规范是大小写不敏感的。#5429 已经修了 extension install source,但 marketplace source 仍会把
HTTPS://...误判成本地路径,从而跳过远程加载。Claude marketplace 插件内部的字符串source也有同类问题。Reviewer Test Plan
如何验证
运行上面的 focused extension tests 和 core validation commands。关键回归用例覆盖大写 GitHub marketplace source、大写 direct JSON marketplace source、大写 HTTP direct JSON marketplace source、大写 marketplace plugin string source,以及大写 npm registry/tarball URL 继续使用共享 helper。
证据(Before & After)
Before:新增的大写 scheme 测试失败,因为
HTTPS://...会被归类为 local 或跳过 HTTP(S) loader。HTTP://...也可能进入远程路径后仍调用https.get,并且 npm/marketplace 各自有一份 protocol-to-client helper。After:
npx vitest run packages/core/src/extension/npm.test.ts packages/core/src/extension/marketplace.test.ts packages/core/src/extension/sourceRegistry.test.ts packages/core/src/extension/claude-converter.test.ts通过,共 120 个测试。额外本地检查也已通过:
Tested on
Environment (optional)
Node.js v26.3.0, npm 11.16.0.
风险和范围
http://direct marketplace JSON source 现在会使用 HTTP client,而不是误走 HTTPS client;npm 和 marketplace 现在共用一个 URL-client helper。/extensionsTUI 端到端交互验证;本 PR 在 core parser/loader 层覆盖。Linked Issues
Fixes #5434
AI Assistance Disclosure
I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.