{"record":{"id":"624014c613b376c9","repo":"MHSanaei/3x-ui","slug":"latest-panel-release-tag-is-empty","errorCode":null,"errorMessage":"latest panel release tag is empty","messagePattern":"latest panel release tag is empty","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/web/service/panel/panel.go","lineNumber":408,"sourceCode":"\t\treturn \"\", fmt.Errorf(\"panel updater download is empty\")\n\t}\n\tif n > maxPanelUpdaterBytes {\n\t\treturn \"\", fmt.Errorf(\"panel updater exceeds %d bytes\", maxPanelUpdaterBytes)\n\t}\n\tif err := file.Chmod(0o700); err != nil {\n\t\treturn \"\", err\n\t}\n\tok = true\n\treturn path, nil\n}\n\nfunc fetchLatestPanelVersion() (string, error) {\n\trelease, err := fetchPanelRelease(\"\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif release.TagName == \"\" {\n\t\treturn \"\", fmt.Errorf(\"latest panel release tag is empty\")\n\t}\n\treturn release.TagName, nil\n}\n\n// fetchPanelRelease fetches a release from GitHub. An empty tag resolves the\n// latest stable release; a non-empty tag (e.g. dev-latest) resolves that tag.\nfunc fetchPanelRelease(tag string) (*service.Release, error) {\n\turl := \"https://api.github.com/repos/MHSanaei/3x-ui/releases/latest\"\n\tif tag != \"\" {\n\t\turl = \"https://api.github.com/repos/MHSanaei/3x-ui/releases/tags/\" + tag\n\t}\n\tclient := (&service.SettingService{}).NewProxiedHTTPClient(10 * time.Second)\n\treq, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)\n\tif reqErr != nil {\n\t\treturn nil, reqErr\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {","sourceCodeStart":390,"sourceCodeEnd":426,"githubUrl":"https://github.com/MHSanaei/3x-ui/blob/ad32144c42455696ea9f14e12168beac3e25f5d2/internal/web/service/panel/panel.go#L390-L426","documentation":"Returned by fetchLatestPanelVersion when the GitHub releases API call succeeded (HTTP 200, JSON decoded) but the tag_name field is empty. The API contract expects every release object to carry tag_name, so an empty value means the response shape changed, a proxy rewrote the body, or a JSON decode partially matched an unexpected payload. It is a sanity guard before the tag is used for update logic.","triggerScenarios":"GET https://api.github.com/repos/MHSanaei/3x-ui/releases/latest returns 200 with a body lacking tag_name; an intercepting proxy returns a 200 JSON status page; a GitHub API change renames the field.","commonSituations":"Man-in-the-middle / filtering proxies that answer API URLs with their own JSON; using an API mirror with a different schema; running an ancient build against a future GitHub API change.","solutions":["curl -s https://api.github.com/repos/MHSanaei/3x-ui/releases/latest | jq .tag_name from the same host to see the raw payload","Bypass or reconfigure the outbound proxy so the genuine GitHub API response arrives","Update the panel to the latest version in case the field handling was fixed upstream"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"var release service.Release\nif err := json.NewDecoder(resp.Body).Decode(&release); err == nil {\n    if release.TagName == \"\" {\n        log.Printf(\"release payload missing tag_name; body shape changed or proxy rewrote it\")\n    }\n}\n","typeGuard":"func hasTagName(r *service.Release) bool {\n    return r != nil && r.TagName != \"\"\n}\n","tryCatchPattern":null,"preventionTips":["Validate required fields of third-party API responses at the boundary","Log the raw payload when contract guards fire, to distinguish proxy rewriting from API change","Pin update flows to API versions/shapes you have tested against"],"tags":["github-api","updater","validation"],"backgroundTag":null,"analyzedSha":"ad32144c42455696ea9f14e12168beac3e25f5d2","analyzedAt":"2026-08-15T11:13:23.905Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}