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

  1. In the offending plugin, set New in ModuleInfo: New: func() caddy.Module { return new(MyModule) }
  2. Rebuild the custom binary with the fixed plugin and re-run `caddy validate`
  3. 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

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.