{"record":{"id":"9a75ed2ebf803aee","repo":"Wei-Shaw/sub2api","slug":"base-url-must-not-include-a-fragment","errorCode":null,"errorMessage":"base URL must not include a fragment","messagePattern":"base URL must not include a fragment","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/internal/pkg/xai/oauth.go","lineNumber":437,"sourceCode":"// normalizeKnownBaseURLPath 规范化 base URL 的 path 部分：\n//   - 官方主机固定使用 /v1 前缀（空 path 自动补齐，其余 path 拒绝）；\n//   - 其他主机保留管理员配置的任意 path 前缀（第三方转发地址常见\n//     /xxx/v1 之类的路由前缀），空 path 仍按惯例补 /v1。\n//\n// 所有主机统一禁止 userinfo/query/fragment，并去除尾部斜杠。\nfunc normalizeKnownBaseURLPath(raw string) (string, error) {\n\tparsed, err := url.Parse(raw)\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn \"\", errors.New(\"invalid base URL\")\n\t}\n\tif parsed.User != nil {\n\t\treturn \"\", errors.New(\"base URL must not include userinfo\")\n\t}\n\tif parsed.ForceQuery || parsed.RawQuery != \"\" {\n\t\treturn \"\", errors.New(\"base URL must not include a query\")\n\t}\n\tif parsed.Fragment != \"\" {\n\t\treturn \"\", errors.New(\"base URL must not include a fragment\")\n\t}\n\tpath := strings.TrimRight(parsed.Path, \"/\")\n\tif path == \"\" {\n\t\tparsed.Path = \"/v1\"\n\t\tparsed.RawPath = \"\"\n\t\treturn strings.TrimRight(parsed.String(), \"/\"), nil\n\t}\n\tif path != \"/v1\" && IsOfficialBaseURLHost(parsed.Hostname()) {\n\t\treturn \"\", fmt.Errorf(\"base URL path must be /v1\")\n\t}\n\tparsed.Path = path\n\tparsed.RawPath = \"\"\n\treturn strings.TrimRight(parsed.String(), \"/\"), nil\n}\n\n// IsOfficialBaseURLHost 报告 host 是否属于官方 API / 区域 API / CLI 网关主机。\nfunc IsOfficialBaseURLHost(host string) bool {\n\thost = strings.ToLower(strings.TrimSpace(host))","sourceCodeStart":419,"sourceCodeEnd":455,"githubUrl":"https://github.com/Wei-Shaw/sub2api/blob/073e92d17178a1ccdb0a27017f572f10c9c7ab62/backend/internal/pkg/xai/oauth.go#L419-L455","documentation":"Thrown by normalizeKnownBaseURLPath when the supplied base URL has a URL fragment ('#...'). Fragments are purely client-side and meaningless in an API base URL, so their presence is treated as a misconfigured value rather than silently ignored.","triggerScenarios":"Passing a base URL like 'https://api.x.ai/v1#section' or 'https://api.x.ai/#' through normalizeKnownBaseURLPath. Parse succeeds but parsed.Fragment is non-empty.","commonSituations":"Base URL copy-pasted from documentation pages where the docs site appends an anchor (#authentication, #rate-limits); users manually trimming a URL but leaving a trailing '#'; template concatenation bugs that glue an anchor onto the endpoint.","solutions":["Strip the fragment from the base URL ('https://api.x.ai/v1#auth' -> 'https://api.x.ai/v1').","Copy the endpoint from the API's raw endpoint list rather than the address bar of a docs page with anchors.","Add a pre-save lint in your config layer that rejects or trims '#'."],"exampleFix":"// before\nbaseURL := \"https://api.x.ai/v1#authentication\"\n\n// after\nbaseURL := \"https://api.x.ai/v1\"","handlingStrategy":"validation","validationCode":"func stripFragment(raw string) string {\n    if i := strings.Index(raw, \"#\"); i >= 0 {\n        return strings.TrimRight(raw[:i], \"/\")\n    }\n    return strings.TrimRight(raw, \"/\")\n}","typeGuard":null,"tryCatchPattern":"if _, err := xai.NormalizeBaseURL(input); err != nil {\n    if strings.Contains(err.Error(), \"fragment\") {\n        input = input[:strings.Index(input, \"#\")] // retry once with fragment stripped\n    }\n}","preventionTips":["Never copy API endpoints from docs pages with anchors","Trim '#' on config ingest","Test config parsing with fragment-bearing input"],"tags":["config","url-validation","xai","oauth"],"backgroundTag":null,"analyzedSha":"073e92d17178a1ccdb0a27017f572f10c9c7ab62","analyzedAt":"2026-08-15T14:33:00.750Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}