gastownhall/beads · error

request failed (attempt %d/%d): %w

Error message

request failed (attempt %d/%d): %w

What it means

This error wraps a transport-level failure from c.HTTPClient.Do() inside the Linear GraphQL client's retry loop (executeOnce, internal/linear/client.go:378). It means the HTTP request never completed — DNS, TCP, TLS, or context cancellation failed before a response status was available. The client retries up to MaxRetries+1 attempts; if all attempts fail, it is surfaced wrapped as 'max retries exceeded'.

Source

Thrown at internal/linear/client.go:378

		if rlErr := c.circuitBreakerError(); rlErr != nil {
			return nil, lastStatus, rlErr
		}

		httpReq, err := http.NewRequestWithContext(ctx, "POST", c.Endpoint, bytes.NewReader(body))
		if err != nil {
			return nil, 0, fmt.Errorf("failed to create request: %w", err)
		}

		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

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check basic connectivity: curl -v https://api.linear.to/graphql from the same host
  2. Inspect the wrapped inner error (errors.Unwrap / %w chain) to distinguish DNS vs timeout vs TLS
  3. Increase the context deadline or HTTPClient.Timeout if attempts die mid-flight
  4. Fix proxy env vars (HTTPS_PROXY) or add the Linear endpoint to NO_PROXY
  5. If offline, fix networking first — the client will keep retrying until MaxRetries is exhausted

Example fix

// before: no timeout, opaque transport failure
client := &linear.Client{Endpoint: "https://api.linear.to/graphql"}
data, err := client.Execute(ctx, req)
// after: explicit timeout + unwrapping the cause
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
data, err := client.Execute(ctx, req)
if err != nil {
    var netErr net.Error
    if errors.As(err, &netErr) && netErr.Timeout() {
        log.Printf("linear request timed out: %v", netErr)
    }
}
Defensive patterns

Strategy: retry

Validate before calling

func checkLinearReachable(ctx context.Context) error {
    c, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    req, _ := http.NewRequestWithContext(c, http.MethodHead, "https://api.linear.to", nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return err }
    resp.Body.Close()
    return nil
}

Type guard

func isTransportFailure(err error) bool {
    var netErr net.Error
    return errors.As(err, &netErr) || strings.Contains(err.Error(), "request failed (attempt")
}

Try / catch

data, err := client.Execute(ctx, req)
if err != nil {
    if strings.Contains(err.Error(), "request failed (attempt") {
        var netErr net.Error
        if errors.As(err, &netErr) && netErr.Timeout() {
            return fmt.Errorf("linear unreachable (timeout): %w", err)
        }
        return fmt.Errorf("linear transport failure: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: c.HTTPClient.Do returns a non-nil error: DNS resolution failure for api.linear.to, connection refused/timeout, TLS handshake failure, proxy misconfiguration, or the request context being canceled mid-request. Each failed attempt appends one of these wrapped errors until retries are exhausted.

Common situations: Offline or air-gapped environments, corporate proxies intercepting TLS to api.linear.to, VPN drops, very short context deadlines, invalid LINEAR_API_URL/endpoint configuration, or IPv6 connectivity issues in containers.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


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