{"record":{"id":"40d7ee147afe7d99","repo":"caddyserver/caddy","slug":"v-additionally-cleanup-v","errorCode":null,"errorMessage":"%v; additionally, cleanup: %v","messagePattern":"(.+?); additionally, cleanup: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"context.go","lineNumber":433,"sourceCode":"\t\tctx.cfg.apps[id] = appModule\n\t\tdefer func() {\n\t\t\tif err != nil {\n\t\t\t\tctx.cfg.failedApps[id] = err\n\t\t\t}\n\t\t}()\n\t}\n\n\tctx.ancestry = append(ctx.ancestry, val)\n\n\tif prov, ok := val.(Provisioner); ok {\n\t\terr = prov.Provision(ctx)\n\t\tif err != nil {\n\t\t\t// incomplete provisioning could have left state\n\t\t\t// dangling, so make sure it gets cleaned up\n\t\t\tif cleanerUpper, ok := val.(CleanerUpper); ok {\n\t\t\t\terr2 := cleanerUpper.Cleanup()\n\t\t\t\tif err2 != nil {\n\t\t\t\t\terr = fmt.Errorf(\"%v; additionally, cleanup: %v\", err, err2)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"provision %s: %v\", modInfo, err)\n\t\t}\n\t}\n\n\tif validator, ok := val.(Validator); ok {\n\t\terr = validator.Validate()\n\t\tif err != nil {\n\t\t\t// since the module was already provisioned, make sure we clean up\n\t\t\tif cleanerUpper, ok := val.(CleanerUpper); ok {\n\t\t\t\terr2 := cleanerUpper.Cleanup()\n\t\t\t\tif err2 != nil {\n\t\t\t\t\terr = fmt.Errorf(\"%v; additionally, cleanup: %v\", err, err2)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"%s: invalid configuration: %v\", modInfo, err)\n\t\t}","sourceCodeStart":415,"sourceCodeEnd":451,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/context.go#L415-L451","documentation":"During LoadModuleByID, if a module's Provision(ctx) fails AND the module implements CleanerUpper, Caddy calls Cleanup() to release dangling state. If that cleanup also errors, both errors are combined: '%v; additionally, cleanup: %v'. The first %v is the provisioning failure; the second is the cleanup failure. This composite is then wrapped by 'provision %s: %v'.","triggerScenarios":"A module's Provision returns an error (bad options, unreachable dependency) and its Cleanup() also fails (e.g. closing already-closed resources, failed connection teardown), so err = fmt.Errorf(\"%v; additionally, cleanup: %v\", err, err2) before being returned from the provisioning path.","commonSituations":"Modules holding open resources (file handles, TLS conns, DNS clients) whose cleanup is unsafe when provisioning died mid-way; plugin bugs where Cleanup assumes Provision completed.","solutions":["Fix the primary provisioning error (the part before '; additionally') — that is the root cause","In your own modules, make Cleanup() idempotent and tolerant of partial provisioning so it never masks the primary error","If the cleanup error comes from a third-party plugin, report it; upgrade the plugin in case it was fixed","Re-run with debug logging (`caddy run --log-level debug`) to get more provisioning context"],"exampleFix":"// before (plugin Cleanup assumes Provision finished)\nfunc (m *M) Cleanup() error { return m.conn.Close() }\n// after\nfunc (m *M) Cleanup() error {\n    if m.conn == nil { return nil }\n    return m.conn.Close()\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if _, err := ctx.LoadModuleByID(id, raw); err != nil {\n    // split the composite if needed: part before '; additionally' is the root cause\n    root := strings.SplitN(err.Error(), \"; additionally\", 2)[0]\n    log.Printf(\"provision failed (root cause): %s\", root)\n    return err\n}","preventionTips":["Write idempotent, nil-safe Cleanup() in custom modules","Never assume Provision completed inside Cleanup","Test the failure path: make Provision error on purpose and assert Cleanup succeeds"],"tags":["caddy","module-system","lifecycle","cleanup","provisioning"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}