{"record":{"id":"1316d4c7416bf4ba","repo":"Wei-Shaw/sub2api","slug":"base-url-must-not-include-a-query","errorCode":null,"errorMessage":"base URL must not include a query","messagePattern":"base URL must not include a query","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/internal/pkg/xai/oauth.go","lineNumber":434,"sourceCode":"\treturn normalizeKnownBaseURLPath(normalized)\n}\n\n// 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","sourceCodeStart":416,"sourceCodeEnd":452,"githubUrl":"https://github.com/Wei-Shaw/sub2api/blob/073e92d17178a1ccdb0a27017f572f10c9c7ab62/backend/internal/pkg/xai/oauth.go#L416-L452","documentation":"Thrown by normalizeKnownBaseURLPath in the xAI OAuth package when the configured base URL contains a query string (either a bare '?' via ForceQuery or actual RawQuery). Base URLs for this package must be scheme + host + optional path only; query strings are rejected outright as a misconfiguration guard.","triggerScenarios":"Calling any API that funnels user-provided base URLs through normalizeKnownBaseURLPath with input like 'https://api.x.ai/v1?foo=bar' or 'https://api.x.ai?'. The url.Parse succeeds, scheme and host are present, but ForceQuery/RawQuery is set.","commonSituations":"Users paste an API endpoint copied from a browser or docs page that includes '?key=...' or tracking params; configuration UIs that append query params for defaults; env vars containing a trailing '?' after template substitution.","solutions":["Remove the query string from the configured base URL (keep scheme://host/path only).","Move any intended parameter (e.g. an API key) into the header/auth field it belongs to, not the base URL.","Sanitize stored configs: strip anything from the first '?' before persisting or calling the normalizer."],"exampleFix":"// before\nbaseURL := \"https://api.x.ai/v1?version=2024-01-01\"\n\n// after\nbaseURL := \"https://api.x.ai/v1\" // version belongs in headers","handlingStrategy":"validation","validationCode":"func sanitizeBaseURL(raw string) (string, error) {\n    u, err := url.Parse(raw)\n    if err != nil || u.Scheme == \"\" || u.Host == \"\" {\n        return \"\", fmt.Errorf(\"invalid base URL %q\", raw)\n    }\n    u.ForceQuery = false\n    u.RawQuery = \"\"\n    u.Fragment = \"\"\n    return strings.TrimRight(u.String(), \"/\"), nil\n}","typeGuard":null,"tryCatchPattern":"normalized, err := xai.NormalizeBaseURL(input)\nif err != nil {\n    return fmt.Errorf(\"please check the xAI base URL config (no query strings allowed): %w\", err)\n}","preventionTips":["Strip everything from the first '?' before storing a base URL","Validate base URLs in config-load tests","Keep auth params in headers, never in the base URL"],"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"}