{"record":{"id":"ee2d262728714aa4","repo":"larksuite/cli","slug":"hook-q-panic-v","errorCode":null,"errorMessage":"hook %q panic: %v","messagePattern":"hook %q panic: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/hook/install.go","lineNumber":261,"sourceCode":"// dispatch) in exchange for total panic isolation.\n//\n// **Factory-local state lifetime contract**: any value the plugin's\n// outer factory captures (`state` in the example above) is now created\n// PER INVOCATION of the wrapped command -- it is NOT a one-shot init\n// the way Plugin.Install is. Plugins that need long-lived state (a\n// connection pool, an LRU cache, a metrics counter) MUST hold it on\n// the Plugin struct or in a package-level variable; relying on\n// closure-local memoisation inside the wrapper factory will silently\n// reset on every command dispatch.\nfunc recoverWrap(fullName string, w platform.Wrapper) platform.Wrapper {\n\treturn func(next platform.Handler) platform.Handler {\n\t\treturn func(ctx context.Context, inv platform.Invocation) (returned error) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t// Preserve the panic value's error identity in the cause\n\t\t\t\t\t// chain when it is an error, so errors.Is/As can still reach\n\t\t\t\t\t// it; fall back to %v formatting for non-error panics.\n\t\t\t\t\tcause := fmt.Errorf(\"hook %q panic: %v\", fullName, r)\n\t\t\t\t\tif e, ok := r.(error); ok {\n\t\t\t\t\t\tcause = fmt.Errorf(\"hook %q panic: %w\", fullName, e)\n\t\t\t\t\t}\n\t\t\t\t\treturned = errs.NewValidationError(errs.SubtypeFailedPrecondition,\n\t\t\t\t\t\t\"hook %q panicked: %v\", fullName, r).\n\t\t\t\t\t\tWithHint(\"plugin hook %q crashed while handling this command; report the panic to the plugin author or remove the plugin\", fullName).\n\t\t\t\t\t\tWithCause(cause)\n\t\t\t\t}\n\t\t\t}()\n\t\t\t// Construct AFTER the recover is armed so a panicking\n\t\t\t// factory becomes a hook envelope instead of a process\n\t\t\t// crash.\n\t\t\tinner := w(next)\n\t\t\treturn inner(ctx, inv)\n\t\t}\n\t}\n}\n","sourceCodeStart":243,"sourceCodeEnd":279,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/hook/install.go#L243-L279","documentation":"recoverWrap wraps a plugin Wrapper so any panic — including one from the wrapper's factory function running at invocation time — is converted into a typed errs validation error (SubtypeFailedPrecondition) with message \"hook %q panicked: %v\" and a hint to report or remove the plugin. The cause chain preserves error identity via %w when the panic value is an error, so errors.Is/As still work. The framework deliberately recovers here so a crashing plugin hook cannot take down the whole CLI process.","triggerScenarios":"A plugin wrapper's factory panics during composition (e.g. mustInit() with bad config) or the wrapped handler panics mid-invocation of a cobra command; fullName is the namespaced hook name (e.g. \"policy-plugin.policy\").","commonSituations":"Plugin initialized with missing/invalid config at factory time; plugin code assuming optional state exists; a plugin written for an older CLI version hitting an incompatible API path at runtime.","solutions":["Read the error's hook name (%q in the message) and the embedded cause to find the panicking plugin/hook.","Fix the plugin: guard factory-time init (the code before `return func(...)` runs per invocation) so it cannot panic on bad config.","Remember factory-captured closure state is recreated per command dispatch; hold long-lived state on the Plugin struct or a package-level variable instead.","If the panic value is an error, use errors.Is/errors.As on the returned error to reach the original typed cause.","As a user (not plugin author), remove or update the offending plugin per the error hint."],"exampleFix":"// before (panicking factory)\nfunc(next platform.Handler) platform.Handler {\n    cfg := mustLoadConfig() // panics when config missing\n    return func(ctx context.Context, inv platform.Invocation) error { ... }\n}\n// after\nfunc(next platform.Handler) platform.Handler {\n    cfg, err := loadConfig()\n    if err != nil {\n        return func(ctx context.Context, inv platform.Invocation) error {\n            return fmt.Errorf(\"policy-plugin: bad config: %w\", err)\n        }\n    }\n    return func(ctx context.Context, inv platform.Invocation) error { ... }\n}","handlingStrategy":"try-catch","validationCode":"// validate plugin config at install time so the wrapper factory never panics:\nif err := plugin.ValidateConfig(cfg); err != nil {\n\treturn fmt.Errorf(\"plugin %s: invalid config: %w\", plugin.Name(), err)\n}","typeGuard":"func isHookPanicValidationError(err error) (hookName string, ok bool) {\n\tvar ve *errs.ValidationError\n\tif !errors.As(err, &ve) || ve.Subtype != errs.SubtypeFailedPrecondition {\n\t\treturn \"\", false\n\t}\n\t// hook name is the first %q in the message; cause keeps error identity\n\treturn ve.HookName, true\n}","tryCatchPattern":"if err := cmd.Execute(); err != nil {\n\tvar ve *errs.ValidationError\n\tif errors.As(err, &ve) && strings.Contains(ve.Error(), \"panicked\") {\n\t\tvar cause error\n\t\tif errors.As(err, &cause) && errors.Is(cause, ErrBadPluginConfig) {\n\t\t\t// recover original typed panic cause via errors.Is/As on the chain\n\t\t}\n\t\tfmt.Fprintln(os.Stderr, \"plugin hook crashed; update or remove the plugin\")\n\t\tos.Exit(1)\n\t}\n}","preventionTips":["Keep wrapper factories panic-free: return errors from the inner handler instead of calling must* helpers at composition time.","Hold long-lived state (pools, caches) on the Plugin struct — factory-captured closures reset per dispatch.","Add recover+error-return in your own wrapper internals during development so panics never reach production dispatch.","Test plugins with missing/invalid config to verify the factory degrades to an error, not a panic."],"tags":["go","panic","hooks","plugins","error-handling"],"backgroundTag":"panic-recovered-in-hook","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}