router-for-me/CLIProxyAPI · error

read response: %w

Error message

read response: %w

What it means

readPluginStoreResponse streams the (2xx) body through an optional LimitReader and io.ReadAll. 'read response: %w' wraps a mid-body I/O failure — the connection dropped or the context was canceled after headers arrived. It is a transport error, not a size or status error (those have their own messages).

Source

Thrown at internal/pluginstore/github.go:281

	defer func() {
		if errClose := resp.Body.Close(); errClose != nil {
			log.WithError(errClose).Debug("failed to close plugin store response body")
		}
	}()
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		if authenticated {
			return nil, fmt.Errorf("unexpected status %d", resp.StatusCode)
		}
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
	}
	reader := io.Reader(resp.Body)
	if maxSize > 0 {
		reader = io.LimitReader(resp.Body, maxSize+1)
	}
	data, errRead := io.ReadAll(reader)
	if errRead != nil {
		return nil, fmt.Errorf("read response: %w", errRead)
	}
	if maxSize > 0 && int64(len(data)) > maxSize {
		return nil, fmt.Errorf("response exceeds maximum allowed size of %d bytes", maxSize)
	}
	return data, nil
}

func pluginStoreRequestError(requestURL string, err error) error {
	parsed, errParse := url.Parse(strings.TrimSpace(requestURL))
	safeURL := "plugin store url"
	if errParse == nil && parsed.Scheme != "" && parsed.Host != "" {
		parsed.User = nil
		parsed.RawQuery = ""
		parsed.ForceQuery = false
		parsed.Fragment = ""
		safeURL = parsed.String()
	}
	var urlError *url.Error

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Retry the download with backoff — mid-body resets are usually transient.
  2. If a context deadline is set on the caller side, raise or remove it for artifact downloads (the repo convention forbids upstream timeouts anyway).
  3. For persistent failures, download the same URL with curl from the same host to isolate proxy/firewall interference.
  4. Check errors.Unwrap for context.Canceled to distinguish user cancellation from network resets.

Example fix

// before
ctx, cancel := context.WithTimeout(ctx, 2*time.Second) // large artifact trips it
client.DownloadArtifact(ctx, artifact) // 'read response: context deadline exceeded'

// after
ctx, cancel := context.WithCancel(ctx) // let the caller decide; no artificial deadline
go func() { time.Sleep(grace); cancel() }()
client.DownloadArtifact(ctx, artifact)
Defensive patterns

Strategy: retry

Try / catch

data, err := client.get(ctx, requestURL, accept, kind, maxSize)
if err != nil {
    if errors.Is(err, context.Canceled) || errors.Is(errors.Unwrap(err), context.Canceled) {
        return err // caller canceled — do not retry
    }
    if strings.Contains(err.Error(), "read response") {
        // transient mid-body reset: retry with backoff, resume-friendly ranges unsupported
        data, err = retryWithBackoff(ctx, func() ([]byte, error) {
            return client.get(ctx, requestURL, accept, kind, maxSize)
        })
    }
    if err != nil { return err }
}

Prevention

When it happens

Trigger: The server or an intermediary closes the connection partway through the body; the caller's context.Context is canceled/times out during the transfer; flaky mobile/high-latency links dropping large artifact downloads.

Common situations: Large release assets interrupted by network churn; contexts with aggressive deadlines; proxies killing long downloads; load balancer idle timeouts.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/2c232fad588d7003. Report an issue: GitHub.