{"record":{"id":"2c232fad588d7003","repo":"router-for-me/CLIProxyAPI","slug":"read-response-w-2c232f","errorCode":null,"errorMessage":"read response: %w","messagePattern":"read response: %w","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/pluginstore/github.go","lineNumber":281,"sourceCode":"\tdefer func() {\n\t\tif errClose := resp.Body.Close(); errClose != nil {\n\t\t\tlog.WithError(errClose).Debug(\"failed to close plugin store response body\")\n\t\t}\n\t}()\n\tif resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {\n\t\tif authenticated {\n\t\t\treturn nil, fmt.Errorf(\"unexpected status %d\", resp.StatusCode)\n\t\t}\n\t\tbody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))\n\t\treturn nil, fmt.Errorf(\"unexpected status %d: %s\", resp.StatusCode, strings.TrimSpace(string(body)))\n\t}\n\treader := io.Reader(resp.Body)\n\tif maxSize > 0 {\n\t\treader = io.LimitReader(resp.Body, maxSize+1)\n\t}\n\tdata, errRead := io.ReadAll(reader)\n\tif errRead != nil {\n\t\treturn nil, fmt.Errorf(\"read response: %w\", errRead)\n\t}\n\tif maxSize > 0 && int64(len(data)) > maxSize {\n\t\treturn nil, fmt.Errorf(\"response exceeds maximum allowed size of %d bytes\", maxSize)\n\t}\n\treturn data, nil\n}\n\nfunc pluginStoreRequestError(requestURL string, err error) error {\n\tparsed, errParse := url.Parse(strings.TrimSpace(requestURL))\n\tsafeURL := \"plugin store url\"\n\tif errParse == nil && parsed.Scheme != \"\" && parsed.Host != \"\" {\n\t\tparsed.User = nil\n\t\tparsed.RawQuery = \"\"\n\t\tparsed.ForceQuery = false\n\t\tparsed.Fragment = \"\"\n\t\tsafeURL = parsed.String()\n\t}\n\tvar urlError *url.Error","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/github.go#L263-L299","documentation":"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).","triggerScenarios":"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.","commonSituations":"Large release assets interrupted by network churn; contexts with aggressive deadlines; proxies killing long downloads; load balancer idle timeouts.","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."],"exampleFix":"// before\nctx, cancel := context.WithTimeout(ctx, 2*time.Second) // large artifact trips it\nclient.DownloadArtifact(ctx, artifact) // 'read response: context deadline exceeded'\n\n// after\nctx, cancel := context.WithCancel(ctx) // let the caller decide; no artificial deadline\ngo func() { time.Sleep(grace); cancel() }()\nclient.DownloadArtifact(ctx, artifact)","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"data, err := client.get(ctx, requestURL, accept, kind, maxSize)\nif err != nil {\n    if errors.Is(err, context.Canceled) || errors.Is(errors.Unwrap(err), context.Canceled) {\n        return err // caller canceled — do not retry\n    }\n    if strings.Contains(err.Error(), \"read response\") {\n        // transient mid-body reset: retry with backoff, resume-friendly ranges unsupported\n        data, err = retryWithBackoff(ctx, func() ([]byte, error) {\n            return client.get(ctx, requestURL, accept, kind, maxSize)\n        })\n    }\n    if err != nil { return err }\n}","preventionTips":["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."],"tags":["network","io","retry","context-cancellation","plugin-store"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}