{"record":{"id":"b8b9eab0986f0095","repo":"router-for-me/CLIProxyAPI","slug":"plugin-client-is-closed","errorCode":null,"errorMessage":"plugin client is closed","messagePattern":"plugin client is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/pluginhost/client_guard.go","lineNumber":62,"sourceCode":"\tcase callResult := <-result:\n\t\tif callResult.recovered != nil {\n\t\t\tpanic(callResult.recovered)\n\t\t}\n\t\treturn callResult.response, callResult.err\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\t}\n}\n\ntype guardedPluginCallResult struct {\n\tresponse  []byte\n\terr       error\n\trecovered any\n}\n\nfunc (c *guardedPluginClient) acquire() (pluginClient, error) {\n\tif c == nil {\n\t\treturn nil, fmt.Errorf(\"plugin client is closed\")\n\t}\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tif c.closed || c.inner == nil {\n\t\treturn nil, fmt.Errorf(\"plugin client is closed\")\n\t}\n\tc.calls++\n\treturn c.inner, nil\n}\n\nfunc (c *guardedPluginClient) release() {\n\tc.mu.Lock()\n\tc.calls--\n\tif c.calls == 0 {\n\t\tc.cond.Broadcast()\n\t}\n\tc.mu.Unlock()\n}","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginhost/client_guard.go#L44-L80","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"// Before issuing the call, confirm host/plugins are still running\nif host == nil || host.IsClosed() {\n    return fmt.Errorf(\"plugin host unavailable; request rejected during teardown\")\n}","typeGuard":"func pluginCallAllowed(h *pluginhost.Host, pluginID string) bool {\n    return h != nil && !h.IsClosed() && h.HasActivePlugin(pluginID)\n}","tryCatchPattern":"resp, err := client.Call(ctx, method, payload)\nif err != nil && strings.Contains(err.Error(), \"plugin client is closed\") {\n    // Do NOT retry: client is closed. Refresh the plugin record and retry once on a new client, or abort.\n    log.Warn(\"plugin client closed mid-request; aborting\")\n    return err\n}","preventionTips":["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."],"tags":["plugin","lifecycle","shutdown","rpc","pluginhost"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}