MHSanaei/3x-ui · error · errRemoteResponseTooLarge

%s %s: %w (cap %d bytes)

Error message

%s %s: %w (cap %d bytes)

What it means

Returned by Remote.do when the streamed body itself exceeds maxRemoteResponseBytes (readCappedBody surfaced errRemoteResponseTooLarge). This is the real guard for the case Content-Length lied, was absent (-1 under transparent decompression), or the body was zstd-compressed on the wire and inflated past the cap during reading.

Source

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

		if msg := bytes.TrimSpace(snippet); len(msg) > 0 {
			// %q quotes/escapes the untrusted node body so control characters or
			// 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
	}

View on GitHub (pinned to ad32144c42)

Solutions

  1. Same triage as the declared-length variant: fetch the endpoint manually and measure the true decompressed size.
  2. Reduce per-node payload (fewer inbounds/clients per node) if the data is legitimate.
  3. Raising maxRemoteResponseBytes is a deliberate capacity decision — measure first, then change the constant in one place in remote.go.
  4. Confirm no proxy is re-chunking responses and defeating the fast-fail path.

Example fix

// before: 200 OK, chunked, 30MB decompressed inbound list
// err: GET panel/api/inbounds/list: remote response too large (cap 8388608 bytes)

// after: distribute inbounds across nodes, or measure and raise the cap
// const maxRemoteResponseBytes = 64 << 20
Defensive patterns

Strategy: validation

Type guard

func isResponseTooLarge(err error) bool {
    return err != nil && strings.Contains(err.Error(), "remote response too large")
}

Try / catch

if err := fetchList(ctx); err != nil {
    if isResponseTooLarge(err) {
        return nil, fmt.Errorf("node %d payload exceeds cap: shard inbounds or raise maxRemoteResponseBytes", nodeID)
    }
    return nil, err
}

Prevention

When it happens

Trigger: Node responds 200 with chunked encoding or Content-Encoding: zstd whose decompressed size exceeds the cap; declared length under the cap but actual bytes over it; a compromised or buggy node streaming unbounded data.

Common situations: Large inbound list served with transparent compression so the wire length is small but the decoded JSON is huge; proxy stripping Content-Length; node bug looping while serializing clients.

Related errors


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