{"record":{"id":"2e4ff87bee252707","repo":"wtfutil/wtf","slug":"failed-to-retrieve-top-clients-check-provided-ap","errorCode":null,"errorMessage":" failed to retrieve top clients: check provided api URL and token\n %s","messagePattern":" failed to retrieve top clients: check provided api URL and token\n (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"modules/pihole/client.go","lineNumber":229,"sourceCode":"\tdefer func() {\n\t\tif closeErr := resp.Body.Close(); closeErr != nil {\n\t\t\treturn\n\t\t}\n\t}()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn tc, fmt.Errorf(\" failed to retrieve version from Pi-hole server\\n http status code: %d\",\n\t\t\tresp.StatusCode)\n\t}\n\n\tvar rBody []byte\n\n\tif rBody, err = io.ReadAll(resp.Body); err != nil {\n\t\treturn tc, fmt.Errorf(\" failed to read top clients response\\n %s\", parseError(err))\n\t}\n\n\tif err = json.Unmarshal(rBody, &tc); err != nil {\n\t\treturn tc, fmt.Errorf(\" failed to retrieve top clients: check provided api URL and token\\n %s\",\n\t\t\tparseError(err))\n\t}\n\n\treturn tc, err\n}\n\ntype QueryTypes struct {\n\tQueryTypes map[string]float32 `json:\"querytypes\"`\n}\n\nfunc getQueryTypes(c http.Client, settings *Settings) (qt QueryTypes, err error) {\n\tvar req *http.Request\n\n\tvar url *url2.URL\n\n\tif url, err = url2.Parse(settings.apiUrl); err != nil {\n\t\treturn qt, fmt.Errorf(\" failed to parse API URL\\n %s\", parseError(err))\n\t}","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/wtfutil/wtf/blob/bb838c1ccb0f0f3223690df44afdec663d622881/modules/pihole/client.go#L211-L247","documentation":"After reading the response, getTopClients unmarshals the body into TopClients{TopSources map[string]int}. This error is thrown when the body is not valid JSON or does not match that shape. The library deliberately hints that the API URL or token is wrong, because invalid auth usually makes Pi-hole return an HTML error page or a different JSON structure.","triggerScenarios":"json.Unmarshal fails on the topClients response: Pi-hole returns HTML (login page or error page) instead of JSON, an empty body, or JSON without a top_sources object because the auth token was rejected.","commonSituations":"Wrong/expired API token causing an HTML response, apiUrl pointing at the web UI instead of api.php, Pi-hole v6 changed the response schema (top_sources shape differs), or a proxy returning an error page.","solutions":["Print/curl the raw response body to see what Pi-hole actually returned","Verify the API token is current (Settings > API in the Pi-hole admin UI) and passed as settings.token","Confirm apiUrl ends at /admin/api.php (the web UI page will return HTML, not JSON)","If running Pi-hole v6+, check whether the response schema still contains top_sources; upgrade or adapt the client struct if not","If the token itself appears fine, inspect whether top_sources changed type (e.g. values as floats) and adjust the TopClients struct"],"exampleFix":"// before: struct may not match v6 schema\ntype TopClients struct {\n    TopSources map[string]int `json:\"top_sources\"`\n}\n\n// after: tolerate float values from newer API\ntype TopClients struct {\n    TopSources map[string]float64 `json:\"top_sources\"`\n}","handlingStrategy":"validation","validationCode":"// pre-flight: confirm the endpoint returns JSON with top_sources\nresp, _ := http.Get(settings.apiUrl + \"auth=\" + settings.token + \"&topClients=1\")\nb, _ := io.ReadAll(resp.Body)\nif !json.Valid(b) || !bytes.Contains(b, []byte(\"top_sources\")) {\n    return fmt.Errorf(\"unexpected pihole response; check token and api.php URL: %.100s\", b)\n}","typeGuard":"func looksLikeTopClients(b []byte) bool {\n    var probe struct {\n        TopSources map[string]int `json:\"top_sources\"`\n    }\n    return json.Unmarshal(b, &probe) == nil\n}","tryCatchPattern":"tc, err := getTopClients(client, settings)\nif err != nil {\n    if strings.Contains(err.Error(), \"check provided api URL and token\") {\n        // body wasn't decodable: dump sanitized body and re-verify token/URL\n        log.Printf(\"non-JSON response from %s; verify token\", redactedURL)\n    }\n    return err\n}","preventionTips":["Validate the token right after generating it with a single curl call","Never point apiUrl at the admin web UI page; it must end at api.php","Re-run a schema smoke test after Pi-hole version upgrades","Log the first ~100 bytes of unexpected bodies (token redacted) to diagnose quickly"],"tags":["json","unmarshal","authentication","go","pihole"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"bb838c1ccb0f0f3223690df44afdec663d622881","analyzedAt":"2026-09-03T17:02:45.030Z","contentChangedAt":"2026-09-03T17:02:45.030Z","schemaVersion":2},"datasetVersion":"2026-09-11T00:17:11.886Z"}