{"record":{"id":"b60f92bacdc14259","repo":"Tencent/WeKnora","slug":"read-response-body-w","errorCode":null,"errorMessage":"read response body: %w","messagePattern":"read response body: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/datasource/connector/yuque/client.go","lineNumber":91,"sourceCode":"\t\t\tlogger.Infof(ctx, \"[Yuque] %s %s (retry %d/%d)\", method, path, attempt, maxRetries)\n\t\t}\n\n\t\tresp, err := c.httpClient.Do(req)\n\t\tif err != nil {\n\t\t\tlastErr = fmt.Errorf(\"execute request: %w\", err)\n\t\t\tif attempt < maxRetries {\n\t\t\t\tif sErr := sleepCtx(ctx, backoff[attempt]); sErr != nil {\n\t\t\t\t\treturn sErr\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn lastErr\n\t\t}\n\n\t\tbody, readErr := io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif readErr != nil {\n\t\t\tlastErr = fmt.Errorf(\"read response body: %w\", readErr)\n\t\t\tif attempt < maxRetries {\n\t\t\t\tif sErr := sleepCtx(ctx, backoff[attempt]); sErr != nil {\n\t\t\t\t\treturn sErr\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn lastErr\n\t\t}\n\n\t\tbodyPreview := truncate(string(body), 500)\n\t\tlogger.Infof(ctx, \"[Yuque] %s %s → status=%d bodyLen=%d body=%s\",\n\t\t\tmethod, path, resp.StatusCode, len(body), bodyPreview)\n\n\t\tif resp.StatusCode == 429 {\n\t\t\twait := parseRetryAfter(resp.Header.Get(\"Retry-After\"), backoff[min(attempt, len(backoff)-1)])\n\t\t\tlastErr = fmt.Errorf(\"yuque rate limited: status=429 body=%s\", bodyPreview)\n\t\t\tif attempt < maxRetries {\n\t\t\t\tif sErr := sleepCtx(ctx, wait); sErr != nil {","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/datasource/connector/yuque/client.go#L73-L109","documentation":"After a successful HTTP response, doRequest reads the whole body with io.ReadAll. If reading fails (connection reset mid-body, premature close, context deadline during read), it wraps the error as \"read response body: %w\" and, if retries remain, backs off and retries the request.","triggerScenarios":"The server or an intermediary (LB/proxy) closes the connection before the full body arrives; network interruption mid-transfer; context deadline exceeded while streaming a large response.","commonSituations":"Unstable networks or flaky VPNs; very large API responses hitting a short context timeout; load balancers with aggressive idle timeouts; TLS interception proxies resetting connections.","solutions":["Retry — the client already retries automatically; check if retries were exhausted and investigate network stability.","Increase the context/request timeout so large bodies can be fully read.","Check intermediary proxies/LBs for connection-reset or idle-timeout issues.","Verify stable connectivity to www.yuque.com (packet loss, MTU issues)."],"exampleFix":"// before\nctx, cancel := context.WithTimeout(ctx, 2*time.Second)\n// after\nctx, cancel := context.WithTimeout(ctx, 30*time.Second) // allow full body read","handlingStrategy":"retry","validationCode":null,"typeGuard":"func isBodyReadError(err error) bool {\n    return strings.Contains(err.Error(), \"read response body:\")\n}","tryCatchPattern":"err := client.ListBookDocs(ctx, repo, page)\nif err != nil && strings.Contains(err.Error(), \"read response body:\") {\n    // transient network issue; client already retried — back off and retry at job level\n    return err\n}","preventionTips":["Use timeouts long enough for the largest expected response.","Prefer stable networks/regions for long-running sync jobs.","Investigate proxies/LBs that reset long transfers.","Log body-length context to distinguish truncation patterns."],"tags":["go","network","http","io","yuque"],"backgroundTag":"connection-reset","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}