wtfutil/wtf · error

failed to read top clients response %s

Error message

 failed to read top clients response
 %s

What it means

getTopClients reads the entire response body with io.ReadAll before JSON decoding. This error is thrown if reading the body fails, e.g. the connection was closed mid-response, a read deadline/timed out, or the server aborted the transfer. parseError wraps the underlying io error with the auth token redacted.

Source

Thrown at modules/pihole/client.go:225

	if resp, err = c.Do(req); err != nil || resp == nil {
		return tc, 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 tc, fmt.Errorf(" failed to retrieve version from Pi-hole server\n http status code: %d",
			resp.StatusCode)
	}

	var rBody []byte

	if rBody, err = io.ReadAll(resp.Body); err != nil {
		return tc, fmt.Errorf(" failed to read top clients response\n %s", parseError(err))
	}

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

	return tc, err
}

type QueryTypes struct {
	QueryTypes map[string]float32 `json:"querytypes"`
}

func getQueryTypes(c http.Client, settings *Settings) (qt QueryTypes, err error) {
	var req *http.Request

	var url *url2.URL

View on GitHub (pinned to bb838c1ccb)

Solutions

  1. Retry the request; transient connection resets often succeed on a second attempt
  2. Increase or remove the http.Client Timeout if Pi-hole responses are slow under load
  3. Check network stability between the client and Pi-hole (packet loss, VPN, Docker NAT)
  4. Verify no proxy/LB is cutting the response early
  5. Look at the wrapped parseError message for the exact I/O cause (e.g. context deadline exceeded, unexpected EOF)

Example fix

// before
c := http.Client{Timeout: 500 * time.Millisecond}

// after
c := http.Client{Timeout: 10 * time.Second}
Defensive patterns

Strategy: retry

Try / catch

tc, err := getTopClients(client, settings)
if err != nil {
    if strings.Contains(err.Error(), "failed to read") {
        // transient I/O: bounded exponential backoff
        for i := 0; i < 3; i++ {
            time.Sleep(time.Duration(1<<i) * time.Second)
            if tc, err = getTopClients(client, settings); err == nil { break }
        }
    }
    return err
}

Prevention

When it happens

Trigger: io.ReadAll(resp.Body) returns an error during the topClients API call: server closes connection mid-body, context/timeout deadline exceeded while streaming, chunked-encoding corruption, or proxy interruption.

Common situations: Flaky Wi-Fi/network to the Pi-hole, an http.Client Timeout too short for a slow Pi-hole under heavy query load, intermediary proxy dropping long responses, or Pi-hole restarting during the request.

Related errors


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