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.ErrorView on GitHub (pinned to 78f0c4079e)
Solutions
- Retry the download with backoff — mid-body resets are usually transient.
- If a context deadline is set on the caller side, raise or remove it for artifact downloads (the repo convention forbids upstream timeouts anyway).
- For persistent failures, download the same URL with curl from the same host to isolate proxy/firewall interference.
- 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
- Do not attach aggressive context deadlines to artifact downloads.
- Distinguish context.Canceled (user intent) from connection resets (retryable) via errors.Is/As.
- Use bounded exponential backoff for retries to avoid hammering a struggling host.
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
- read response: %w
- read Claude OAuth %s response: %w
- decode Claude OAuth %s response: %w
- failed to read token response: %w
- failed to read refresh response: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/2c232fad588d7003.
Report an issue: GitHub.