gastownhall/beads · error

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

Error message

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

What it means

io.ReadAll failed while draining the Linear GraphQL response body, capped at MaxResponseSize via io.LimitReader (internal/linear/client.go:385). The HTTP round trip succeeded, but the response stream broke mid-read. Like transport errors, this is retried up to MaxRetries+1 times and only surfaced if every attempt fails.

Source

Thrown at internal/linear/client.go:385

		}

		httpReq.Header.Set("Content-Type", "application/json")
		authValue, err := c.authHeader()
		if err != nil {
			return nil, 0, err
		}
		httpReq.Header.Set("Authorization", authValue)

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

		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
		}

		lastStatus = resp.StatusCode
		rl := parseRateLimitHeaders(resp.Header)
		c.recordRateLimitHeaders(rl)

		if resp.StatusCode == http.StatusTooManyRequests {
			delay := rl.RetryAfter
			if delay == 0 {
				delay = RetryDelay * time.Duration(1<<attempt) // Exponential backoff
				if half := int64(delay / 2); half > 0 {
					delay += time.Duration(rand.Int64N(half)) //nolint:gosec // G404: jitter for retry backoff does not need crypto rand
				}
			} else if delay > MaxRetryAfterDelay {
				fmt.Fprintf(os.Stderr, "linear: Retry-After %v exceeds cap %v; using cap\n", delay, MaxRetryAfterDelay)
				delay = MaxRetryAfterDelay
			}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the operation — transient mid-body resets usually succeed on a fresh connection
  2. Reduce response size with pagination/limits in the GraphQL query
  3. Check for proxies or load balancers truncating large responses
  4. Inspect the wrapped error for 'connection reset' vs 'context deadline exceeded' and fix accordingly

Example fix

// before: unbounded query that streams a huge body
query := `{ issues { nodes { id title description comments { nodes { body } } } } }`
data, err := client.Execute(ctx, &linear.GraphQLRequest{Query: query})
// after: paginate to keep each response small
query := `query($after: String) { issues(first: 50, after: $after) { pageInfo { hasNextPage endCursor } nodes { id title } } }`
Defensive patterns

Strategy: retry

Type guard

func isResponseReadError(err error) bool {
    return strings.Contains(err.Error(), "failed to read response (attempt")
}

Try / catch

data, err := client.Execute(ctx, req)
if err != nil {
    if strings.Contains(err.Error(), "failed to read response") {
        return retryWithBackoff(ctx, req) // transient mid-body break; retry once or twice
    }
    return err
}

Prevention

When it happens

Trigger: The connection is reset or times out while streaming the response body, the server closes the connection prematurely, or a proxy/keep-alive race truncates the stream so io.ReadAll returns an error.

Common situations: Flaky networks or mobile/VPN links, aggressive load balancers with short idle timeouts, large GraphQL payloads (big issue exports) exceeding proxy body limits, or keep-alive connections reused after a long idle period.

Related errors


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