sipeed/picoclaw · warning

Failed to read request body

Error message

Failed to read request body

What it means

HTTP 400 returned by POST /api/models (handleAddModel) when io.ReadAll(io.LimitReader(r.Body, 1<<20)) returns an error. This is a transport-level failure while streaming the request body, not a payload problem: connection reset mid-upload, invalid chunked transfer encoding, or a client abort. Bodies larger than 1 MiB are silently truncated by LimitReader (surfacing later as 'Invalid JSON'), not rejected here.

Source

Thrown at web/backend/api/models.go:311

		})
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]any{
		"models":           models,
		"total":            len(models),
		"default_model":    defaultModel,
		"provider_options": modelProviderOptionsForResponse(),
	})
}

// handleAddModel appends a new model configuration entry.
//
//	POST /api/models
func (h *Handler) handleAddModel(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		http.Error(w, "Failed to read request body", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	type custom struct {
		config.ModelConfig
		APIKey string `json:"api_key"`
	}

	var mc custom
	if err = json.Unmarshal(body, &mc); err != nil {
		http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
		return
	}

	normalizeIncomingModelConfig(&mc.ModelConfig)

	if err = validateIncomingModelConfig(&mc.ModelConfig, nil); err != nil {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Retry the request from the client; transient connection resets are the usual cause
  2. Keep request bodies well under the 1 MiB cap so uploads complete quickly
  3. If reproducible, bypass intermediate proxies to see whether one is cutting the body
  4. Verify the client sends a correct Content-Length or valid chunked framing
Defensive patterns

Strategy: retry

Try / catch

async function postModel(url, body, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      const res = await fetch(url, { method: 'POST', body });
      if (res.ok || res.status !== 400 || !res.statusText.includes('body')) return res;
      throw new Error(await res.text());
    } catch (e) {
      if (i === attempts - 1) throw e; // transient body-read failures
      await new Promise(r => setTimeout(r, 300 * (i + 1)));
    }
  }
}

Prevention

When it happens

Trigger: POST /api/models where the TCP connection drops partway through the upload, a proxy (nginx/cloudflare) terminates a slow upload, the client uses malformed chunked encoding, or curl is interrupted mid-request.

Common situations: Flaky networks on large model payloads (big extra_body blobs), aggressive proxy timeouts, browser tab closed during submit, or a middleware/proxy in front of the backend mangling transfer-encoding.

Related errors


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