gastownhall/beads · error

failed to read response (attempt %d/%d): %w

Error message

failed to read response (attempt %d/%d): %w

What it means

Reading the response body with io.ReadAll (bounded to 50MB via io.LimitReader) failed on this attempt. The error is recorded as lastErr with attempt counters and the loop retries; if all attempts fail it surfaces via the 'max retries exceeded' wrapper.

Source

Thrown at internal/gitlab/client.go:142

		if err != nil {
			return nil, nil, fmt.Errorf("failed to create request: %w", err)
		}

		req.Header.Set("PRIVATE-TOKEN", c.Token)
		req.Header.Set("Content-Type", "application/json")

		resp, err := c.HTTPClient.Do(req)
		if err != nil {
			lastErr = fmt.Errorf("request failed (attempt %d/%d): %w", attempt+1, MaxRetries+1, err)
			continue
		}

		// Limit response body to 50MB to prevent OOM from malformed responses.
		const maxResponseSize = 50 * 1024 * 1024
		respBody, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
		_ = resp.Body.Close() // Best effort: HTTP body close; connection may be reused regardless
		if err != nil {
			lastErr = fmt.Errorf("failed to read response (attempt %d/%d): %w", attempt+1, MaxRetries+1, err)
			continue
		}

		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			return respBody, resp.Header, nil
		}

		// Retry on rate-limiting and server errors with exponential backoff.
		retriable := resp.StatusCode == http.StatusTooManyRequests ||
			resp.StatusCode == http.StatusInternalServerError ||
			resp.StatusCode == http.StatusBadGateway ||
			resp.StatusCode == http.StatusServiceUnavailable ||
			resp.StatusCode == http.StatusGatewayTimeout

		if retriable {
			delay := RetryDelay * time.Duration(1<<attempt)
			useServerDelay := false

View on GitHub (pinned to 71377f2769)

Solutions

  1. Simply retrying often helps - the client already retries; if final failure persists, check network stability
  2. Reduce page size / use the filter parameters so responses are smaller and faster to read
  3. Check for middleboxes (proxy/VPN) resetting long-lived connections
  4. Inspect the wrapped lastErr in 'max retries exceeded' to confirm it's a read/body error

Example fix

// before
issues, _, err := client.ListIssues(ctx, filter) // flaky network, body read fails
// after
if err != nil { time.Sleep(backoff); issues, _, err = client.ListIssues(ctx, filter) } // caller-level retry with backoff
Defensive patterns

Strategy: retry

Validate before calling

// no pre-call validation possible; ensure stable network
if err := checkConnectivity(host); err != nil { return err }

Type guard

var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() { /* mid-body timeout */ }

Try / catch

if err != nil && strings.Contains(err.Error(), "failed to read response") {
	time.Sleep(jitteredBackoff(attempt))
	retry()
}

Prevention

When it happens

Trigger: The connection was reset or timed out while streaming the response body (server closed mid-response, network blip, keep-alive race), causing the underlying read to return a transport error.

Common situations: Unstable network or flaky LB dropping connections mid-response; GitLab server OOM/restart while responding; aggressive intermediate proxies cutting long responses; very large issue list responses over slow links.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/fdaa2f13a93e7746. Report an issue: GitHub.