router-for-me/CLIProxyAPI · error

decode plugin envelope %s: %w

Error message

decode plugin envelope %s: %w

What it means

callPlugin in internal/pluginhost/rpc_client.go received a response from the out-of-process plugin but json.Unmarshal of the raw bytes into pluginabi.Envelope failed. Every plugin RPC reply must be a JSON envelope ({ok, result, error}); anything else — HTML error pages, plain-text output, a crashed/exiting plugin's partial write, or a protocol-version mismatch — lands here.

Source

Thrown at internal/pluginhost/rpc_client.go:161

	if resp.Capabilities.ManagementAPI {
		plugin.Capabilities.ManagementAPI = adapter
	}
	return plugin, nil
}

func callPlugin[T any](ctx context.Context, client pluginClient, method string, request any) (T, error) {
	var zero T
	rawRequest, errMarshal := json.Marshal(sanitizePluginRequest(request))
	if errMarshal != nil {
		return zero, fmt.Errorf("marshal plugin request %s: %w", method, errMarshal)
	}
	rawResp, errCall := client.Call(ctx, method, rawRequest)
	if errCall != nil {
		return zero, errCall
	}
	var envelope pluginabi.Envelope
	if errUnmarshal := json.Unmarshal(rawResp, &envelope); errUnmarshal != nil {
		return zero, fmt.Errorf("decode plugin envelope %s: %w", method, errUnmarshal)
	}
	out, errDecode := decodeEnvelopeResult[T](envelope)
	if errDecode != nil {
		if !envelope.OK {
			return zero, errDecode
		}
		return zero, fmt.Errorf("decode plugin result %s: %w", method, errDecode)
	}
	return out, nil
}

func sanitizePluginRequest(request any) any {
	switch req := request.(type) {
	case pluginapi.AuthLoginStartRequest:
		req.HTTPClient = nil
		return req
	case pluginapi.AuthLoginPollRequest:
		req.HTTPClient = nil

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Run the plugin binary standalone and ensure its stdout carries only the JSON envelope (route logs to stderr)
  2. Rebuild/reinstall the plugin against the same version as the host so pluginabi framing matches
  3. Check the plugin's own logs/exit status for a crash during the call
  4. If a wrapper script is used, silence its banners or have it exec the plugin directly

Example fix

// plugin: before
fmt.Println("starting up") // pollutes stdout envelope stream

// plugin: after
fmt.Fprintln(os.Stderr, "starting up") // logs go to stderr only
Defensive patterns

Strategy: retry

Try / catch

resp, err := callPlugin[Result](ctx, client, "Method", req)
if err != nil && strings.Contains(err.Error(), "decode plugin envelope") {
    client.Restart() // plugin stdout is polluted or process is wedged
    resp, err = callPlugin[Result](ctx, client, "Method", req)
    if err != nil {
        return fmt.Errorf("plugin protocol broken after restart: %w", err)
    }
}

Prevention

When it happens

Trigger: client.Call returns bytes that are not valid JSON or not an envelope object: plugin wrote to stdout instead of the RPC channel, plugin crashed mid-response, plugin built against an older/newer pluginabi with a different framing, or a wrapper script printed extra output.

Common situations: Plugin executable prints debug logs to stdout; a shell wrapper (npx, pip run) prepends warnings; plugin and host built from different CLIProxyAPI versions; plugin panics after writing a partial line.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/99d2e4ba80a1538e. Report an issue: GitHub.