router-for-me/CLIProxyAPI · error

plugin client is closed

Error message

plugin client is closed

What it means

guardedPluginClient.acquire returns this error when a plugin RPC call is attempted on a client that has been closed (or whose inner transport is nil). The guard serializes calls with a mutex and reference count; once Close() runs, every subsequent acquire fails fast with this error instead of touching a dead transport. This is the c == nil early return.

Source

Thrown at internal/pluginhost/client_guard.go:62

	case callResult := <-result:
		if callResult.recovered != nil {
			panic(callResult.recovered)
		}
		return callResult.response, callResult.err
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

type guardedPluginCallResult struct {
	response  []byte
	err       error
	recovered any
}

func (c *guardedPluginClient) acquire() (pluginClient, error) {
	if c == nil {
		return nil, fmt.Errorf("plugin client is closed")
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed || c.inner == nil {
		return nil, fmt.Errorf("plugin client is closed")
	}
	c.calls++
	return c.inner, nil
}

func (c *guardedPluginClient) release() {
	c.mu.Lock()
	c.calls--
	if c.calls == 0 {
		c.cond.Broadcast()
	}
	c.mu.Unlock()
}

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Treat this as a shutdown/teardown signal, not a transient error: do not retry the same call on the same client.
  2. If seen during normal operation (not shutting down), check whether the plugin crashed and got fused/closed — look for preceding panic or fuse log entries.
  3. Ensure your code stops issuing plugin calls after host Close/Shutdown returns; use the host's lifecycle hooks to drain in-flight work before teardown.
  4. If it appears during hot-reload, re-acquire the plugin record (activeRecords) so subsequent calls go to the new client instance.
Defensive patterns

Strategy: type-guard

Validate before calling

// Before issuing the call, confirm host/plugins are still running
if host == nil || host.IsClosed() {
    return fmt.Errorf("plugin host unavailable; request rejected during teardown")
}

Type guard

func pluginCallAllowed(h *pluginhost.Host, pluginID string) bool {
    return h != nil && !h.IsClosed() && h.HasActivePlugin(pluginID)
}

Try / catch

resp, err := client.Call(ctx, method, payload)
if err != nil && strings.Contains(err.Error(), "plugin client is closed") {
    // Do NOT retry: client is closed. Refresh the plugin record and retry once on a new client, or abort.
    log.Warn("plugin client closed mid-request; aborting")
    return err
}

Prevention

When it happens

Trigger: Any plugin capability call (execute, auth, command-line) issued after the host has shut down the plugin, after the plugin crashed and its client was closed, or concurrently with host teardown/reload. Also when calling acquire on a nil *guardedPluginClient.

Common situations: Hot-reload of plugins while requests are in flight; graceful shutdown racing an in-progress request retry; a fused plugin whose client was closed but a queued call still references it.

Related errors


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