sipeed/picoclaw · error

read error response: %w

Error message

read error response: %w

What it means

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.

Source

Thrown at cmd/picoclaw/internal/model/online.go:50

	url := strings.TrimRight(baseURL, "/") + "/models"

	client := &http.Client{Timeout: 15 * time.Second}
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, readErr := io.ReadAll(io.LimitReader(resp.Body, 512))
		if readErr != nil {
			return nil, fmt.Errorf("read error response: %w", readErr)
		}
		return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}

	// {"data": [...]} envelope. Distinguish "envelope shape with empty list"
	// from "object without a data key" via Data being non-nil after unmarshal:
	// json.Unmarshal sets Data to []modelEntry{} for `{"data":[]}` but leaves
	// it as nil when "data" is absent or null.
	var envelope modelsAPIResponse
	if err := json.Unmarshal(body, &envelope); err == nil && envelope.Data != nil {
		return envelope.Data, nil
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Retry the operation — mid-body drops on error responses are nearly always transient.
  2. If it reproduces, capture the exchange with curl -v to see where the body is cut.
  3. Report to the provider if every error response dies mid-body.
Defensive patterns

Strategy: retry

Try / catch

body, readErr := io.ReadAll(io.LimitReader(resp.Body, 512))
if readErr != nil {
    if isTransientConnError(readErr) { // net.ErrClosed / syscall.ECONNRESET
        return retryWithBackoff()
    }
    return fmt.Errorf("read error response: %w", readErr)
}

Prevention

When it happens

Trigger: 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.

Common situations: Flaky upstream, an aggressive load balancer resetting errored connections, or a proxy that truncates error bodies; rare and almost always transient.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/62b65dce233f0cf0. Report an issue: GitHub.