{"record":{"id":"ae1ee5a09b977657","repo":"bytebase/bytebase","slug":"s-must-not-carry-a-query-string","errorCode":null,"errorMessage":"%s must not carry a query string","messagePattern":"(.+?) must not carry a query string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/common/util.go","lineNumber":142,"sourceCode":"func NormalizeExternalURL(externalURL string) (string, error) {\n\tr := strings.TrimSpace(externalURL)\n\tr = strings.TrimSuffix(r, \"/\")\n\tu, err := url.Parse(r)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"%s malformed\", externalURL)\n\t}\n\tscheme := strings.ToLower(u.Scheme)\n\tif scheme != \"http\" && scheme != \"https\" {\n\t\treturn \"\", errors.Errorf(\"%s must start with http:// or https://\", externalURL)\n\t}\n\tif u.Host == \"\" {\n\t\treturn \"\", errors.Errorf(\"%s must name a host\", externalURL)\n\t}\n\tif u.User != nil {\n\t\treturn \"\", errors.Errorf(\"%s must not carry userinfo\", externalURL)\n\t}\n\tif u.RawQuery != \"\" || u.ForceQuery {\n\t\treturn \"\", errors.Errorf(\"%s must not carry a query string\", externalURL)\n\t}\n\tif u.Fragment != \"\" || u.RawFragment != \"\" {\n\t\treturn \"\", errors.Errorf(\"%s must not carry a fragment\", externalURL)\n\t}\n\n\thost := strings.ToLower(u.Host)\n\tport := u.Port()\n\tif port != \"\" {\n\t\t// The external URL is used as the redirectURL in the get token process of OAuth, and the\n\t\t// RedirectURL needs to be consistent with the RedirectURL in the get code process.\n\t\t// The frontend gets it through window.location.origin in the get code\n\t\t// process, so port 80/443 need to be cropped.\n\t\tif (scheme == \"http\" && port == \"80\") || (scheme == \"https\" && port == \"443\") {\n\t\t\thost = strings.ToLower(u.Hostname())\n\t\t\tif strings.Contains(host, \":\") {\n\t\t\t\thost = \"[\" + host + \"]\"\n\t\t\t}\n\t\t}","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/bytebase/bytebase/blob/1870550677fe08f0d2a78c07acd27541464eb945/backend/common/util.go#L124-L160","documentation":"NormalizeExternalURL rejects URLs that contain a query string (RawQuery non-empty or ForceQuery set, i.e. a trailing \"?\"). The external URL is used as an OAuth redirect base, so a query component would corrupt redirect URLs.","triggerScenarios":"Calling NormalizeExternalURL with \"https://example.com/?utm_source=x\" or \"https://example.com/?\"; reached via the workspace external URL validation call sites.","commonSituations":"Copying a full page URL including tracking parameters (utm_*, session IDs) instead of the bare origin.","solutions":["Strip everything from \"?\" onward; configure only scheme://host[:port][/path].","Copy the origin from the browser address bar up to (but not including) the \"?\".","Re-run the workspace settings validation after cleaning the URL."],"exampleFix":"// before\nNormalizeExternalURL(\"https://example.com/?utm_source=x\")\n// after\nNormalizeExternalURL(\"https://example.com\")","handlingStrategy":"validation","validationCode":"u, _ := url.Parse(strings.TrimSpace(externalURL))\nif u.RawQuery != \"\" || u.ForceQuery {\n  return errors.New(\"external URL must not contain a query string; strip everything after '?'\")\n}","typeGuard":null,"tryCatchPattern":"normalized, err := common.NormalizeExternalURL(u)\nif err != nil && strings.Contains(err.Error(), \"must not carry a query string\") {\n  if cleaned := strings.SplitN(u, \"?\", 2)[0]; cleaned != \"\" {\n    normalized, err = common.NormalizeExternalURL(cleaned)\n  }\n}","preventionTips":["Copy only the origin (scheme://host[:port][/path]) from the address bar","Strip utm_* and other tracking parameters before configuring","Automatically cut at '?' in admin tooling that accepts URLs"],"tags":["url","query-string","configuration"],"backgroundTag":"invalid-url-format","analyzedSha":"1870550677fe08f0d2a78c07acd27541464eb945","analyzedAt":"2026-09-06T21:16:13.665Z","contentChangedAt":"2026-09-06T21:16:13.665Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}