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
- Treat this as a shutdown/teardown signal, not a transient error: do not retry the same call on the same client.
- 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.
- 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.
- 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
- Drain in-flight plugin calls before host Close()/Shutdown().
- Never cache plugin clients across reload boundaries; re-resolve via activeRecords.
- Treat 'plugin client is closed' as permanent for that client — never blind-retry.
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
- plugin host is unavailable
- host model executor is unavailable
- decode host log request: %w
- auth file not found
- Codex Alpha Search API key base URL unavailable
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/b8b9eab0986f0095.
Report an issue: GitHub.