{"record":{"id":"d75d710e32d28adb","repo":"router-for-me/CLIProxyAPI","slug":"prepare-plugin-write-w","errorCode":null,"errorMessage":"prepare plugin write: %w","messagePattern":"prepare plugin write: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/pluginstore/install.go","lineNumber":293,"sourceCode":"\t\texistingData, errReadExisting := os.ReadFile(targetPath)\n\t\tif errReadExisting != nil {\n\t\t\treturn InstallResult{}, fmt.Errorf(\"read target plugin: %w\", errReadExisting)\n\t\t}\n\t\tif bytes.Equal(existingData, libraryData) {\n\t\t\treturn InstallResult{\n\t\t\t\tID:          id,\n\t\t\t\tVersion:     strings.TrimSpace(plugin.Version),\n\t\t\t\tPath:        targetPath,\n\t\t\t\tOverwritten: true,\n\t\t\t\tSkipped:     true,\n\t\t\t}, nil\n\t\t}\n\t}\n\t// Re-check immediately before replacing an existing file: the same version\n\t// may have been loaded while the archive was being downloaded and verified.\n\tif overwritten && options.BeforeWrite != nil {\n\t\tif errBeforeWrite := options.BeforeWrite(); errBeforeWrite != nil {\n\t\t\treturn InstallResult{}, fmt.Errorf(\"prepare plugin write: %w\", errBeforeWrite)\n\t\t}\n\t}\n\tif overwritten && loadedPluginInstallBlocked(options) {\n\t\treturn InstallResult{}, ErrLoadedPluginLocked\n\t}\n\tif errWrite := writeFileAtomic(targetPath, libraryData, mode); errWrite != nil {\n\t\treturn InstallResult{}, errWrite\n\t}\n\treturn InstallResult{\n\t\tID:          id,\n\t\tVersion:     strings.TrimSpace(plugin.Version),\n\t\tPath:        targetPath,\n\t\tOverwritten: overwritten,\n\t}, nil\n}\n\nfunc installTargetPath(options InstallOptions, id string, version string) (string, error) {\n\tversion = normalizeVersion(version)","sourceCodeStart":275,"sourceCodeEnd":311,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/install.go#L275-L311","documentation":"Wrapped error from InstallArchive when the caller-supplied options.BeforeWrite hook returns an error. BeforeWrite is invoked only when overwriting an already-installed plugin, as a last-chance consistency gate right before the atomic file replacement (e.g. to flush/stop the loaded plugin). Its error is surfaced verbatim under 'prepare plugin write' and cancels the write.","triggerScenarios":"Passing InstallOptions{BeforeWrite: func() error {...}} whose body fails: a hook that tries to unload/unregister a live plugin and gets a busy/timeout error, or one that re-checks state and finds a conflicting concurrent update.","commonSituations":"Hooks that quiesce a running process before upgrade and fail when the process refuses shutdown; race with another installer that changed state between the initial stat and the write; hook code with its own bugs (nil map, closed channel) returning errors.","solutions":["Make BeforeWrite tolerant of the states you consider safe (e.g. treat 'already unloaded' as success, return nil)","Serialize concurrent installs of the same plugin with a lock so the hook's assumptions hold","Return nil from the hook when the precondition you guard against is actually acceptable, and let the idempotency byte-compare handle no-op installs"],"exampleFix":"// before\noptions.BeforeWrite = func() error {\n    return livePlugins.Unload(id) // errors if plugin already gone or busy\n}\n\n// after\noptions.BeforeWrite = func() error {\n    if err := livePlugins.Unload(id); err != nil {\n        if errors.Is(err, errNotLoaded) {\n            return nil // nothing to quiesce; safe to write\n        }\n        return err\n    }\n    return nil\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"options.BeforeWrite = func() error {\n    if err := quiesce(plugin.ID); err != nil && !errors.Is(err, errAlreadyQuiet) {\n        return err\n    }\n    return nil\n}\nif _, err := pluginstore.InstallArchive(data, plugin, options); err != nil {\n    if strings.Contains(err.Error(), \"prepare plugin write:\") {\n        // hook vetoed: plugin still loaded; unload properly and retry\n    }\n}","preventionTips":["Make BeforeWrite idempotent and tolerant of already-quiet states","Serialize installs of the same plugin ID with a mutex","Keep hook bodies minimal: unload/flush only, no side effects that can fail spuriously"],"tags":["go","plugin-store","callback","install","concurrency"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}