{"record":{"id":"6895e215d583a922","repo":"router-for-me/CLIProxyAPI","slug":"plugin-client-is-closed-6895e2","errorCode":null,"errorMessage":"plugin client is closed","messagePattern":"plugin client is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/pluginhost/loader_windows.go","lineNumber":255,"sourceCode":"\t\treturn false\n\t}\n\tfile, errOpen := os.Open(path)\n\tif errOpen != nil {\n\t\treturn false\n\t}\n\tdefer func() {\n\t\t_ = file.Close()\n\t}()\n\thasher := sha256.New()\n\tif _, errCopy := io.Copy(hasher, file); errCopy != nil {\n\t\treturn false\n\t}\n\treturn hex.EncodeToString(hasher.Sum(nil)) == digest\n}\n\nfunc (c *dynamicLibraryClient) Call(ctx context.Context, method string, request []byte) ([]byte, error) {\n\tif c == nil || c.api.call == 0 {\n\t\treturn nil, fmt.Errorf(\"plugin client is closed\")\n\t}\n\tif ctx != nil {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tdefault:\n\t\t}\n\t}\n\tmethodBytes, errMethod := syscall.BytePtrFromString(method)\n\tif errMethod != nil {\n\t\treturn nil, errMethod\n\t}\n\tvar requestPtr uintptr\n\tif len(request) > 0 {\n\t\trequestPtr = uintptr(unsafe.Pointer(&request[0]))\n\t}\n\tresponseMem, errAlloc := windows.LocalAlloc(\n\t\twindows.LMEM_FIXED|windows.LMEM_ZEROINIT,","sourceCodeStart":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginhost/loader_windows.go#L237-L273","documentation":"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.","triggerScenarios":"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.","commonSituations":"Plugin hot-reload during config changes; management API triggering plugin reload while traffic is being served; cached client references not refreshed after reload.","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"],"exampleFix":"// before\nresp, err := winClient.Call(ctx, method, req)\n\n// after\nclient, ok := host.LookupPlugin(id)\nif !ok || client == nil {\n    return nil, fmt.Errorf(\"plugin %s unavailable\", id)\n}\nresp, err := client.Call(ctx, method, req)","handlingStrategy":"try-catch","validationCode":"client, ok := host.LookupPlugin(pluginID)\nif !ok || client == nil {\n    return fmt.Errorf(\"plugin %s is not available\", pluginID)\n}","typeGuard":null,"tryCatchPattern":"out, err := client.Call(ctx, method, body)\nif err != nil && strings.Contains(err.Error(), \"plugin client is closed\") {\n    client, ok := host.LookupPlugin(pluginID)\n    if !ok {\n        return nil, err\n    }\n    out, err = client.Call(ctx, method, body)\n}","preventionTips":["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"],"tags":["plugin","windows","lifecycle","dynamic-library"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}