wtfutil/wtf · error

failed to retrieve top items: check provided api URL and to

Error message

 failed to retrieve top items: check provided api URL and token
 %s

What it means

getTopItems in modules/pihole/client.go wraps a json.Unmarshal failure while decoding the topItems response body into the TopItems struct. The library throws it because the server returned a body that is not JSON of the expected shape (note the code also ignores io.ReadAll's error here).

Source

Thrown at modules/pihole/client.go:160

		return ti, fmt.Errorf(" failed to connect to Pi-hole server\n %s", parseError(err))
	}

	defer func() {
		if closeErr := resp.Body.Close(); closeErr != nil {
			return
		}
	}()

	if resp.StatusCode >= http.StatusBadRequest {
		return ti, fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d",
			resp.StatusCode)
	}

	var rBody []byte

	rBody, err = io.ReadAll(resp.Body)
	if err = json.Unmarshal(rBody, &ti); err != nil {
		return ti, fmt.Errorf(" failed to retrieve top items: check provided api URL and token\n %s",
			parseError(err))
	}

	return ti, err
}

type TopClients struct {
	TopSources map[string]int `json:"top_sources"`
}

// parseError removes any token from output and ensures a non-nil response
func parseError(err error) string {
	if err == nil {
		return "unknown error"
	}

	var re = regexp.MustCompile(`auth=[a-zA-Z0-9]*`)

View on GitHub (pinned to bb838c1ccb)

Solutions

  1. Curl the exact request URL (with auth and topItems params) and inspect the raw body to see what is actually returned.
  2. Fix settings.token so Pi-hole returns real topItems data instead of an auth error.
  3. Check your Pi-hole version; for v6, update the client/structs to match the new API schema or use a compatible client version.
  4. Remove any proxy that injects HTML pages into API responses.

Example fix

// before
settings.token = "" // auth error JSON returned
// after
settings.token = os.Getenv("PIHOLE_WEBPASSWORD")
Defensive patterns

Strategy: try-catch

Validate before calling

resp, err := http.Get(settings.apiUrl + "?auth=" + settings.token + "&topItems=10")
if err == nil {
    body, _ := io.ReadAll(resp.Body)
    var probe map[string]json.RawMessage
    if json.Unmarshal(body, &probe) != nil {
        return errors.New("topItems endpoint did not return expected JSON")
    }
}

Try / catch

if err := client.GetTopItemsView(ctx); err != nil {
    if strings.Contains(err.Error(), "failed to retrieve top items") {
        log.Printf("topItems response not parseable — check token and Pi-hole version: %v", err)
        return
    }
    return err
}

Prevention

When it happens

Trigger: In getTopItems (via getTopItemsView), json.Unmarshal(rBody, &ti) fails: response is HTML, empty, an auth-error JSON object, or a Pi-hole API version whose topItems schema differs from the TopItems struct.

Common situations: Invalid auth token returning an auth error payload; Pi-hole v6 API returning a different JSON schema than this legacy client expects; an HTML error page from a reverse proxy; empty body due to API parameter mismatch.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of wtfutil/wtf@bb838c1ccb (2026-09-03). Data as JSON: /api/errors/f6e2429119af2b24. Report an issue: GitHub.