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 = nilView on GitHub (pinned to 78f0c4079e)
Solutions
- Run the plugin binary standalone and ensure its stdout carries only the JSON envelope (route logs to stderr)
- Rebuild/reinstall the plugin against the same version as the host so pluginabi framing matches
- Check the plugin's own logs/exit status for a crash during the call
- 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
- Keep plugin stdout reserved for the RPC envelope; send all plugin logs to stderr
- Build plugins and host from the same release so the envelope schema matches
- Avoid wrapper scripts that print banners before exec-ing the plugin
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
- decode host auth list request: %w
- decode host auth get request: %w
- decode host auth get runtime request: %w
- decode host auth save request: %w
- decode host log request: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/99d2e4ba80a1538e.
Report an issue: GitHub.