{"record":{"id":"62b65dce233f0cf0","repo":"sipeed/picoclaw","slug":"read-error-response-w","errorCode":null,"errorMessage":"read error response: %w","messagePattern":"read error response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/picoclaw/internal/model/online.go","lineNumber":50,"sourceCode":"\turl := strings.TrimRight(baseURL, \"/\") + \"/models\"\n\n\tclient := &http.Client{Timeout: 15 * time.Second}\n\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"build request: %w\", err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+apiKey)\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, readErr := io.ReadAll(io.LimitReader(resp.Body, 512))\n\t\tif readErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"read error response: %w\", readErr)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"HTTP %d: %s\", resp.StatusCode, strings.TrimSpace(string(body)))\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read response: %w\", err)\n\t}\n\n\t// {\"data\": [...]} envelope. Distinguish \"envelope shape with empty list\"\n\t// from \"object without a data key\" via Data being non-nil after unmarshal:\n\t// json.Unmarshal sets Data to []modelEntry{} for `{\"data\":[]}` but leaves\n\t// it as nil when \"data\" is absent or null.\n\tvar envelope modelsAPIResponse\n\tif err := json.Unmarshal(body, &envelope); err == nil && envelope.Data != nil {\n\t\treturn envelope.Data, nil\n\t}\n","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/cmd/picoclaw/internal/model/online.go#L32-L68","documentation":"After receiving a non-200 response, reading its body (capped at 512 bytes via io.LimitReader) failed. The server sent a status line but the connection broke before the error body could be read, so the code wraps and returns the read error instead of the status diagnostics.","triggerScenarios":"The connection is closed or reset between the response headers and body of an error response while io.ReadAll(io.LimitReader(resp.Body, 512)) runs at online.go:50 — e.g. an intermediary RST after sending a 4xx/5xx status.","commonSituations":"Flaky upstream, an aggressive load balancer resetting errored connections, or a proxy that truncates error bodies; rare and almost always transient.","solutions":["Retry the operation — mid-body drops on error responses are nearly always transient.","If it reproduces, capture the exchange with curl -v to see where the body is cut.","Report to the provider if every error response dies mid-body."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"body, readErr := io.ReadAll(io.LimitReader(resp.Body, 512))\nif readErr != nil {\n    if isTransientConnError(readErr) { // net.ErrClosed / syscall.ECONNRESET\n        return retryWithBackoff()\n    }\n    return fmt.Errorf(\"read error response: %w\", readErr)\n}","preventionTips":["Classify mid-body read failures as transient and retry once before surfacing.","Log resp.StatusCode alongside the read error to identify offending intermediaries."],"tags":["network","http","transient","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}