router-for-me/CLIProxyAPI · error
plugin client is closed
Error message
plugin client is closed
What it means
Windows counterpart of the unix closed-client guard: dynamicLibraryClient.Call() refuses to run when c is nil or the plugin's call pointer is 0, i.e. the client was shut down (or never completed Open). Note that on Windows, Shutdown() deliberately does not unload the DLL, but it still nils out the function pointers, so later Call() attempts fail here.
Source
Thrown at internal/pluginhost/loader_windows.go:255
return false
}
file, errOpen := os.Open(path)
if errOpen != nil {
return false
}
defer func() {
_ = file.Close()
}()
hasher := sha256.New()
if _, errCopy := io.Copy(hasher, file); errCopy != nil {
return false
}
return hex.EncodeToString(hasher.Sum(nil)) == digest
}
func (c *dynamicLibraryClient) Call(ctx context.Context, method string, request []byte) ([]byte, error) {
if c == nil || c.api.call == 0 {
return nil, fmt.Errorf("plugin client is closed")
}
if ctx != nil {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
}
methodBytes, errMethod := syscall.BytePtrFromString(method)
if errMethod != nil {
return nil, errMethod
}
var requestPtr uintptr
if len(request) > 0 {
requestPtr = uintptr(unsafe.Pointer(&request[0]))
}
responseMem, errAlloc := windows.LocalAlloc(
windows.LMEM_FIXED|windows.LMEM_ZEROINIT,View on GitHub (pinned to 78f0c4079e)
Solutions
- Fetch the plugin client from the host registry per call rather than caching it across reloads
- Serialize Shutdown() with in-flight Call()s (wait-group or RWMutex) in your integration code
- Treat this error as a signal to re-resolve the plugin and retry once
Example fix
// before
resp, err := winClient.Call(ctx, method, req)
// after
client, ok := host.LookupPlugin(id)
if !ok || client == nil {
return nil, fmt.Errorf("plugin %s unavailable", id)
}
resp, err := client.Call(ctx, method, req) Defensive patterns
Strategy: try-catch
Validate before calling
client, ok := host.LookupPlugin(pluginID)
if !ok || client == nil {
return fmt.Errorf("plugin %s is not available", pluginID)
} Try / catch
out, err := client.Call(ctx, method, body)
if err != nil && strings.Contains(err.Error(), "plugin client is closed") {
client, ok := host.LookupPlugin(pluginID)
if !ok {
return nil, err
}
out, err = client.Call(ctx, method, body)
} Prevention
- Look up the client per request instead of caching across reloads
- Treat 'plugin client is closed' as a reload race signal and re-resolve once
When it happens
Trigger: Calling Call() after Shutdown(); using a client whose Open() failed (closeAfterOpenFailure zeroes the pointers); racing a config hot-reload that shuts plugins down while a request dispatches.
Common situations: Plugin hot-reload during config changes; management API triggering plugin reload while traffic is being served; cached client references not refreshed after reload.
Related errors
- plugin client is closed
- cliproxy_plugin_init returned %d: %v
- plugin ABI version %d is not supported
- plugin function table is incomplete
- remove stale shadow plugin: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/6895e215d583a922.
Report an issue: GitHub.