fix: convert emit HTTP2Options-shaped h2-opts for share-link HTTP/2 transport#2737
Merged
wwqgtxx merged 2 commits intoApr 22, 2026
Merged
Conversation
…ransport The case "h2" block in common/convert/v.go was dead code from b23a071 until PR MetaCubeX#2694 renamed the switch label and activated it. Activation exposed a copy-paste mistake from case "http": h2-opts.path was emitted as []string while HTTP2Options.Path is string, and h2-opts.headers was populated although HTTP2Options has no Headers field. The path mismatch makes adapter.ParseProxy drop every VMess/VLESS node whose share link uses type=http; the headers residue is silently ignored by the decoder but serves no purpose. Align the h2 branch with HTTP2Options exactly (host + path, nothing else). Update the existing converter unit test whose expectation locked in the buggy shape. Add an end-to-end regression test in a new adapter/parser_test.go that runs convert.ConvertsV2Ray through adapter.ParseProxy so future drift in any h2-opts field is caught at the contract boundary. Fixes MetaCubeX#2555
This was referenced Apr 21, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要 / Summary
修复 #2555。带
type=http(HTTP/2 transport)的 VMess / VLESS 分享链接在导入后会静默消失:common/convert/v.go产出的h2-optsmap 把path写成[]string,但下游HTTP2Options.Path类型是string,所以adapter.ParseProxy解码失败、整个节点被丢弃。本 PR 修正类型错配,并移除同一次 copy-paste 带来的
headers死代码残留,然后补充端到端回归测试锁住convert → ParseProxy链路契约。Fixes #2555. VMess / VLESS share links with
type=http(HTTP/2 transport) silently fail to import: the converter incommon/convert/v.goemits anh2-optsmap whosepathis[]string, butHTTP2Options.Pathisstring, soadapter.ParseProxyrejects the entire node during decoding.This PR fixes the type mismatch, removes the dead
headersresidue from the same copy-paste, and adds an end-to-end regression test that locks the converter-to-ParseProxycontract.复现方式 / Reproduction
最小复现用的 VLESS 分享链接:
Minimal VLESS share link that triggers the bug:
在当前 Alpha 分支上,经过
convert.ConvertsV2Ray→adapter.ParseProxy会返回:On current Alpha, running this through
convert.ConvertsV2Ray→adapter.ParseProxyreturns:节点被静默丢弃。
The node is silently dropped from the subscription.
根因 / Root cause
common/convert/v.go:93-104当前产出:common/convert/v.go:93-104currently emits:但
adapter/outbound/vmess.go:80-83的 schema 是:But
adapter/outbound/vmess.go:80-83declares:common/structure.decodeString(被adapter.ParseProxy调用)在WeaklyTypedInput模式下只接受string / int / uint / float,不会把 slice 转成 string。所以h2-opts.path解码失败,ParseProxy返回 error,节点被丢弃。common/structure.decodeString(invoked byadapter.ParseProxy) accepts onlystring / int / uint / floatunderWeaklyTypedInput; it does not convert[]string → string. The decode ofh2-opts.pathfails andParseProxyreturns an error, discarding the node.h2Opts["headers"] = headers也是一个 no-op:HTTP2Options没有Headers字段。由于common/structure没启用ErrorUnused,这个未知键会被静默忽略——对解码不构成问题,但残留在代码里毫无意义。The
h2Opts["headers"] = headersassignment is additionally a no-op:HTTP2Optionshas noHeadersfield. Sincecommon/structuredoes not enableErrorUnused, the unknown key is silently ignored — harmless for decoding, but pure noise in the source.问题如何引入 / How it was introduced
b23a0710(2022) 首次添加case "h2":block,当时 switch 标签写的是case "http":。但函数顶部network已被 remap 成"h2",所以 switch 的case "http":永远匹配不到——整个 block 从出生那天起就是不可达的死代码。path类型错没人发现,因为分支从不执行。PR fix(convert): normalize VLESS share-link transport mapping #2694 /
0495d295(2026-04-13) 修正了这个控制流错配,把 case 标签改为"h2":,激活了死代码。[]string类型错从此第一次真正生效,用户开始遇到节点消失。b23a0710(2022) introduced thecase "h2":block, originally labelledcase "http":. At the top of the same function,network = "http"is remapped to"h2", but the switch case was literal"http". As a result the entire block was unreachable dead code from the day it was written. The wrongpathtype went unnoticed because the branch never ran.PR fix(convert): normalize VLESS share-link transport mapping #2694 / commit
0495d295(2026-04-13) fixed the control-flow mismatch by renaming the case to"h2":, activating the dead code for the first time. The[]stringtype error only began affecting users at that point.Issue #2555 提交于 2026-02-04,在 #2694 合并之前。报告人通过静态阅读代码就发现了两处问题;#2694 修了控制流那一半,本 PR 修类型那一半。
Issue #2555 was filed 2026-02-04, before #2694 was merged. The reporter caught both problems by static reading; #2694 fixed the control-flow half, this PR fixes the type half.
为什么 block 满是 copy-paste 残留 / Why the block is full of copy-paste residue
case "h2":几乎是相邻case "http":(HTTP/1.1 伪装)的逐行镜像,但两个目标 struct 的 schema 不同:The
case "h2":block is a near-verbatim clone of the adjacentcase "http":(HTTP/1.1 camouflage), but the two target structs have different schemas:HTTPOptions(HTTP/1.1 camouflage)HTTP2Options(HTTP/2 transport)Path[]stringstringHeadersmap[string][]string(真字段,实际被消费)所以从
case "http":直接复制一份到case "h2":就会带上两处残留:[]string类型的path和永不被消费的headersmap。两者自 2022 年起潜伏,直到 #2694 把分支唤醒才暴露。A direct copy from
case "http":tocase "h2":therefore carries two artifacts: the slice-typedpathand the never-consumedheadersmap. Both have been latent since 2022 and surfaced only after #2694 woke up the branch.为什么
HTTP2Options没有Headers字段 / WhyHTTP2Optionshas noHeadersfield不是遗漏——是 5 年以上一致的设计:
Not an omission — it is the consistent design across 5+ years:
adapter/outbound/vmess.go:80——HTTP2Optionsstruct 在 2020 年 commit5bd189f2引入,精确Host+Path两个字段。之后从未变过。transport/vmess/h2.go:23—— transport 层的H2Config也精确是Hosts+Path。transport/vmess/h2.go:28-49——establishConn()构造的 HTTP/2 请求Method硬编码为"PUT",Header 硬编码为唯一一条Accept-Encoding: identity。根本没有读用户 H2 header 的代码路径。adapter/outbound/vmess.go:80—HTTP2Optionsstruct was introduced in 2020 (5bd189f2) with exactlyHost+Path. Never changed.transport/vmess/h2.go:23— the transport-layerH2Configalso has exactlyHosts+Path.transport/vmess/h2.go:28-49—establishConn()builds the HTTP/2 request with a hardcodedMethod: "PUT"and a single hardcoded headerAccept-Encoding: identity. There is no code path that reads user-supplied H2 headers.对比有
Headers字段的兄弟:HTTPOptions(HTTP/1.1 伪装——需要伪造 Host/User-Agent 让流量看起来像明文 HTTP)和WSOptions(WebSocket——握手本身是 HTTP upgrade,自定义 header 用于 CDN 路由)。HTTP/2 的伪装锚点在外层 TLS SNI / ALPN,不在 HTTP/2 frame 的 header 层,这正是项目始终没把自定义 H2 header 接进来的原因。Contrast with the siblings that do carry a
Headersfield:HTTPOptions(HTTP/1.1 camouflage — needs Host/User-Agent forging to look like plain HTTP) andWSOptions(WebSocket — HTTP upgrade handshake naturally uses custom headers for CDN routing). HTTP/2 disguise is anchored at the outer TLS SNI / ALPN layer, not inside the HTTP/2 frame headers, which is why the project never wired custom H2 headers through.修复 / The fix
common/convert/v.gocase "h2":(+2 / -4)产出的 map 现在精确对齐
HTTP2Optionsschema——只含host和path,无多余字段、无类型错配。The resulting map now precisely matches the
HTTP2Optionsschema — exactlyhostandpath, nothing else.common/convert/converter_test.go(+2 / -3)TestConvertsV2RayVlessHTTPTransportUsesH2Opts之前把 buggy 形状(path为[]string、以及空headersmap)锁在断言里。同步更新期望值,使之对齐修复后的输出。TestConvertsV2RayVlessHTTPTransportUsesH2Optspreviously locked in the buggy shape (pathas[]string, plus the deadheadersmap). Updated the expected map to match the corrected output.adapter/parser_test.go(+24, new)新增的回归测试跑完整
convert.ConvertsV2Ray → adapter.ParseProxy链路,锁住"converter 产物必须能被下游解码"的契约:Added a regression test that runs the full
convert.ConvertsV2Ray → adapter.ParseProxychain, locking the contract that the converter's output must be decodable by the typed config parser:已验证:
git stash回退v.go修复后,此测试以 issue 报告里的完全相同错误信息 FAIL;恢复后 PASS。这道测试不仅覆盖path本身,还能防止未来h2-opts任何字段的类型漂移。Verified that reverting the
v.gofix (viagit stash) makes this test fail with the exact same error from the issue report; restoring the fix makes it pass. This guards against future type drift in anyh2-optsfield, not justpath.放在
adapter_test外部测试包里,避开common/convert → adapter的反向 import cycle,同时仍通过公开ParseProxy接口做端到端验证。Placed in
adapter_test(external test package) to avoid thecommon/convert → adapterimport cycle while still exercising the publicParseProxysurface.为什么顺手清理
headers残留而不是只改path/ Why clean up theheadersresidue instead of a minimal path-only fix只改
path类型就能让节点解析通过,但会留下两行与pathbug 源自同一次 copy-paste 的死代码。本次清理严格在同一个case "h2":block 内,不触碰任何相邻代码,让分支的最终形状精确镜像HTTP2Optionsschema。反之,如果留着h2Opts["headers"] = headers,将来读这段代码的人每次都要重新确认一遍"解码器容忍这个键吗?"——这不是可靠性问题,但是代码卫生问题。A one-line type fix would make the node parse, but it would leave two lines of dead code that were born from the same copy-paste event as the
pathbug. The cleanup stays strictly inside the samecase "h2":block — no neighboring code touched — and makes the branch's final shape mirror theHTTP2Optionsschema exactly. Leavingh2Opts["headers"] = headersin would create semantic noise: a future reader would have to verify, again, that the decoder tolerates it.不在本 PR 范围 / Out of scope (follow-ups for separate PRs)
common/convert/converter.go:333-343— legacy base64-JSON VMesscase "h2":存在一个平行 bug:host被写进了不存在的headers["Host"]key 而不是h2Opts["host"],通过这条入口导入的节点伪装 host 会被静默丢弃。同一 copy-paste 根源,但入口(v2rayN 老式 base64 订阅 vs 新式 VMess AEAD URL)不同,本 PR 不扩散。已单独跟踪:[Bug] legacy base64-JSON VMess subscription silently drops disguise host for h2 transport #2738。common/convert/converter.go:333-343— legacy base64-JSON VMesscase "h2":has a parallel bug:hostis written into a non-existentheaders["Host"]key instead ofh2Opts["host"], so the disguise host is silently dropped on that entry path. Same copy-paste origin as this PR, but a different entry point (legacy v2rayN base64 subscription vs modern VMess AEAD URL). Tracked separately: [Bug] legacy base64-JSON VMess subscription silently drops disguise host for h2 transport #2738.相关 / Related
Fixes [Bug] 当 network 的值为 http 时,v.go 解析错误 #2555
根因分析(中文)已发在 issue 评论:[Bug] 当 network 的值为 http 时,v.go 解析错误 #2555 (comment)
本 bug 的控制流那一半由 fix(convert): normalize VLESS share-link transport mapping #2694 修复。
Fixes [Bug] 当 network 的值为 http 时,v.go 解析错误 #2555
Chinese root-cause breakdown posted on the issue: [Bug] 当 network 的值为 http 时,v.go 解析错误 #2555 (comment)
The control-flow half of the same bug was fixed by fix(convert): normalize VLESS share-link transport mapping #2694.