{"record":{"id":"f5636fbfbf8c6992","repo":"caddyserver/caddy","slug":"module-already-registered-s","errorCode":null,"errorMessage":"module already registered: %s","messagePattern":"module already registered: (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"modules.go","lineNumber":158,"sourceCode":"\n\tif mod.ID == \"\" {\n\t\tpanic(\"module ID missing\")\n\t}\n\tif mod.ID == \"caddy\" || mod.ID == \"admin\" {\n\t\tpanic(fmt.Sprintf(\"module ID '%s' is reserved\", mod.ID))\n\t}\n\tif mod.New == nil {\n\t\tpanic(\"missing ModuleInfo.New\")\n\t}\n\tif val := mod.New(); val == nil {\n\t\tpanic(\"ModuleInfo.New must return a non-nil module instance\")\n\t}\n\n\tmodulesMu.Lock()\n\tdefer modulesMu.Unlock()\n\n\tif _, ok := modules[string(mod.ID)]; ok {\n\t\tpanic(fmt.Sprintf(\"module already registered: %s\", mod.ID))\n\t}\n\tmodules[string(mod.ID)] = mod\n}\n\n// GetModule returns module information from its ID (full name).\nfunc GetModule(name string) (ModuleInfo, error) {\n\tmodulesMu.RLock()\n\tdefer modulesMu.RUnlock()\n\tm, ok := modules[name]\n\tif !ok {\n\t\treturn ModuleInfo{}, fmt.Errorf(\"module not registered: %s\", name)\n\t}\n\treturn m, nil\n}\n\n// GetModuleName returns a module's name (the last label of its ID)\n// from an instance of its value. If the value is not a module, an\n// empty string will be returned.","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/modules.go#L140-L176","documentation":"The global module registry is keyed by string ID; registering the same ID twice panics. This is almost always caused by a plugin being linked into the binary twice (different import paths after forking/vendoring) or two modules that chose the same ID.","triggerScenarios":"Importing the same plugin package under two paths (e.g. original plus fork), or two distinct packages both registering ID \"http.handlers.custom\"; init() of both runs RegisterModule with the same ID.","commonSituations":"Using xcaddy with a fork while the original is still pulled by another dependency; renaming a module package but keeping its ID while both get linked; duplicate builds after vendoring.","solutions":["Run caddy list-modules --packages to find which import paths register the colliding ID, then remove or replace one","In go.mod, use a replace directive to point both import paths at a single copy of the plugin","If you own the module, give forks a distinct ID"],"exampleFix":"# before (two copies linked)\ngo mod edit -replace github.com/original/plugin=./fork\n# keep only one; in go.mod of the consuming module:\n# after\nrequire github.com/original/plugin v1.2.3\nreplace github.com/original/plugin => github.com/myorg/plugin v1.2.3","handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":["Check binary size/plugin list for accidental duplicate linking","Use go.mod replace to dedupe fork and original","Give forked modules distinct IDs"],"tags":["caddy","go","module-system","duplicate-registration","build","panic"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}