caddyserver/caddy · error
module '%s' has no constructor
Error message
module '%s' has no constructor
What it means
A module is registered in the registry (its ID exists) but its ModuleInfo.New field is nil, so LoadModuleByID cannot instantiate it. This is always a bug in the registering code: caddy.RegisterModule was called with a ModuleInfo lacking the New constructor function.
Source
Thrown at context.go:373
// returns the value. If mod.New is nil, an error is returned.
// If the module implements Validator or Provisioner interfaces,
// those methods are invoked to ensure the module is fully
// configured and valid before being used.
//
// This is a lower-level method and will usually not be called
// directly by most modules. However, this method is useful when
// dynamically loading/unloading modules in their own context,
// like from embedded scripts, etc.
func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error) {
modulesMu.RLock()
modInfo, ok := modules[id]
modulesMu.RUnlock()
if !ok {
return nil, fmt.Errorf("unknown module: %s", id)
}
if modInfo.New == nil {
return nil, fmt.Errorf("module '%s' has no constructor", modInfo.ID)
}
val := modInfo.New()
// value must be a pointer for unmarshaling into concrete type, even if
// the module's concrete type is a slice or map; New() *should* return
// a pointer, otherwise unmarshaling errors or panics will occur
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Pointer {
log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+
" so we are using reflection to make a pointer instead; please fix this by"+
" using new(Type) or &Type notation in your module's New() function.", id)
val = reflect.New(rv.Type()).Elem().Addr().Interface().(Module)
}
// fill in its config only if there is a config to fill in
if len(rawMsg) > 0 {
err := StrictUnmarshalJSON(rawMsg, &val)
if err != nil {View on GitHub (pinned to 50e54ee279)
Solutions
- In the offending plugin, set New in ModuleInfo: New: func() caddy.Module { return new(MyModule) }
- Rebuild the custom binary with the fixed plugin and re-run `caddy validate`
- Report/patch upstream if the plugin is third-party and not yours
Example fix
// before
func init() {
caddy.RegisterModule(caddy.ModuleInfo{ID: "http.handlers.broken"})
}
// after
func init() {
caddy.RegisterModule(caddy.ModuleInfo{
ID: "http.handlers.broken",
New: func() caddy.Module { return new(BrokenHandler) },
})
} Defensive patterns
Strategy: validation
Validate before calling
// Plugin self-check at init time: register only complete ModuleInfos
func init() {
info := caddy.ModuleInfo{ ID: "http.handlers.foo", New: func() caddy.Module { return new(Foo) } }
if info.New == nil { panic("ModuleInfo.New must be set") }
caddy.RegisterModule(info)
} Type guard
func hasConstructor(info caddy.ModuleInfo) bool { return info.New != nil } Try / catch
if _, err := ctx.LoadModuleByID(id, raw); err != nil { return fmt.Errorf("module %s unusable: %w", id, err) } // constructor bug is a plugin defect Prevention
- Always set New when calling caddy.RegisterModule
- Copy the canonical module registration boilerplate from Caddy docs
- Add a smoke test that loads each of your registered modules with empty config
When it happens
Trigger: A plugin calls caddy.RegisterModule(ModuleInfo{ID: "..."}) without supplying New: func() caddy.Module {...}; any attempt to load that module (config referencing it, or ctx.LoadModuleByID) hits the modInfo.New == nil branch.
Common situations: Hand-written or generated plugin boilerplate that omits the New field; copy-paste from an example that used a different RegisterModule signature; refactoring that accidentally deleted the New closure.
Related errors
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/56d9cc405ea9a9e0.
Report an issue: GitHub.