MHSanaei/3x-ui · error

read body: %w

Error message

read body: %w

What it means

Returned by Remote.do when reading the response body fails for a reason OTHER than the size cap — i.e. a transport-level failure mid-body. The status was 200 and headers were fine, but the stream broke before completion. The wrapped error is the raw io/network error from the body reader.

Source

Thrown at internal/web/runtime/remote.go:281

			// newlines in it can't garble or inject into the error/log output.
			return nil, fmt.Errorf("%s %s: HTTP %d: %q", method, path, resp.StatusCode, msg)
		}
		return nil, fmt.Errorf("%s %s: HTTP %d", method, path, resp.StatusCode)
	}

	// Fast-fail on an honestly-declared oversize body; the LimitReader below is
	// the real guard since Content-Length is untrusted, may be absent, or is -1
	// under transparent decompression.
	if resp.ContentLength > maxRemoteResponseBytes {
		return nil, fmt.Errorf("%s %s: %w (content-length %d, cap %d)", method, path, errRemoteResponseTooLarge, resp.ContentLength, maxRemoteResponseBytes)
	}

	raw, err := readCappedBody(resp.Body, maxRemoteResponseBytes)
	if err != nil {
		if errors.Is(err, errRemoteResponseTooLarge) {
			return nil, fmt.Errorf("%s %s: %w (cap %d bytes)", method, path, err, maxRemoteResponseBytes)
		}
		return nil, fmt.Errorf("read body: %w", err)
	}

	var env envelope
	if err := json.Unmarshal(raw, &env); err != nil {
		return nil, fmt.Errorf("decode envelope: %w", err)
	}
	if !env.Success {
		return &env, &remoteAPIError{msg: env.Msg}
	}
	return &env, nil
}

func (r *Remote) resolveRemoteID(ctx context.Context, tag string) (int, error) {
	if id, ok := r.cacheGetTag(tag); ok {
		return id, nil
	}
	if err := r.refreshRemoteIDs(ctx); err != nil {
		return 0, err

View on GitHub (pinned to ad32144c42)

Solutions

  1. Retry the operation — mid-body failures are usually transient; the runtime marks the node dirty and ReconcileNode will converge state anyway.
  2. If it recurs on the same node, check the node's logs for a crash (OOM, panic) at that timestamp.
  3. Raise proxy read timeouts between master and node if large payloads consistently get cut.
  4. Inspect the wrapped cause: 'context deadline exceeded' means the RPC timeout is too small for the payload, not a network fault.

Example fix

// before: 10s RPC timeout, 20MB inbound list over slow link
// err: read body: context deadline exceeded

// after: raise the per-call timeout budget in the caller (context.WithTimeout) or reduce payload size
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
Defensive patterns

Strategy: retry

Type guard

func isMidBodyFailure(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "read body:") && !isResponseTooLarge(err)
}

Try / catch

err := withRetry(3, func() error {
    e, err := r.do(ctx, method, path, body)
    env = e
    return err
}) // only retry when isMidBodyFailure(err)

Prevention

When it happens

Trigger: Connection reset by the node or an intermediate proxy while streaming a large response; node process killed mid-response; TLS renegotiation failure; idle-timeout on a proxy cutting long transfers; context cancelled during the read.

Common situations: Flaky network path between master and node; proxy idle/response timeout shorter than the time to stream a big inbound list; node OOM-killed during serialization of a large payload.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/63be470db6dfb876. Report an issue: GitHub.