MHSanaei/3x-ui · error

decode envelope: %w

Error message

decode envelope: %w

What it means

Returned by Remote.do when the 200 response body is not valid JSON for the wire envelope ({success, msg, obj}). The node answered OK at the HTTP layer but the payload is not the expected protocol — most commonly an HTML error/login page, a plaintext proxy message, or truncated output. It indicates protocol mismatch, not a node-side business error.

Source

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

	// 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
	}
	if id, ok := r.cacheGetTag(tag); ok {
		return id, nil
	}
	return 0, fmt.Errorf("remote inbound with tag %q not found on node %s", tag, r.node.Name)

View on GitHub (pinned to ad32144c42)

Solutions

  1. Manually reproduce: curl -H 'Authorization: ...' https://node/panel/api/inbounds/list and look at the raw body — if it is HTML, the request never reached the API.
  2. Re-save the node's credential on the master so the auth header the runtime sends is accepted by the node's middleware.
  3. Fix the node address to point at the panel port, not the subscription/other listener.
  4. Align master and node versions if the envelope shape changed between releases.

Example fix

// before: wrong port (subscription server) configured as node address
// err: decode envelope: invalid character '<' looking for beginning of value

// after: point the node record at the panel's HTTPS port serving /panel/api/*
Defensive patterns

Strategy: validation

Validate before calling

// Smoke-test the wire contract once per node at registration
env, err := r.do(ctx, http.MethodGet, "panel/api/inbounds/list", nil)
if err != nil { return err } // a decode-envelope failure here means auth/path is wrong

Type guard

func isEnvelopeDecodeError(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "decode envelope:")
}

Try / catch

if err := r.do(ctx, method, path, body); err != nil {
    if isEnvelopeDecodeError(err) {
        return fmt.Errorf("node %s speaks wrong protocol (auth redirect or wrong port): %w", r.node.Name, err)
    }
    return err
}

Prevention

When it happens

Trigger: The master's request hit the node's web LOGIN page (session middleware redirected to /login and returned 200 HTML) because the RPC auth header/token was missing or rejected; a reverse proxy served a 200 status page; the body was truncated by an intermediary; node version speaks a different envelope.

Common situations: Node API token expired/rotated so auth middleware silently redirects; proxy rewrites the path and serves an SPA index.html with 200; master pointed at the node's subscription port instead of the panel port.

Related errors


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