caddyserver/caddy · error

unrecognized type for module: %s

Error message

unrecognized type for module: %s

What it means

Context.LoadModule decides how to load nested modules based on the reflected type of the destination field. It handles pointers to modules, json.RawMessage slices (flat and nested), ModuleMap types, and maps. If the field's kind matches none of these, it falls through to default and returns 'unrecognized type for module: %s' with the Go type string. This is a developer error: the struct field tagged with a caddy module tag has a type the loader cannot process.

Source

Thrown at context.go:280

			for i := 0; i < val.Len(); i++ {
				thisSet, err := ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val.Index(i))
				if err != nil {
					return nil, err
				}
				all = append(all, thisSet)
			}
			result = all
		}

	case reflect.Map:
		// val is a ModuleMap or some other kind of map
		result, err = ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val)
		if err != nil {
			return nil, err
		}

	default:
		return nil, fmt.Errorf("unrecognized type for module: %s", typ)
	}

	// we're done with the raw bytes; allow GC to deallocate
	val.Set(reflect.Zero(typ))

	return result, nil
}

// emitEvent is a small convenience method so the caddy core can emit events, if the event app is configured.
func (ctx Context) emitEvent(name string, data map[string]any) Event {
	if ctx.cfg == nil || ctx.cfg.eventEmitter == nil {
		return Event{}
	}
	return ctx.cfg.eventEmitter.Emit(ctx, name, data)
}

// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any.
// Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Change the struct field type to one the loader supports: *SomeModule (pointer to registered module), caddy.ModuleMap, map[string]json.RawMessage, []json.RawMessage, or [][]json.RawMessage
  2. If the field is not supposed to hold a module, remove the caddy:"module=..." struct tag so LoadModule skips it
  3. Rebuild the plugin/binary and re-run config validation

Example fix

// before
type MyHandler struct {
    Upstream http.Upstream `json:"upstream"` // module tag elsewhere, non-pointer value type
}
// after
type MyHandler struct {
    Upstream caddyhttp.UpstreamHandler `json:"upstream"` // use pointer/module-compatible type
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Plugin build-time: assert fields tagged as modules have loadable types
func assertModuleFieldType(t reflect.Type) error {
    switch {
    case t.Kind() == reflect.Pointer,
         isModuleMapType(t),
         isJSONRawMessage(t):
        return nil
    }
    return fmt.Errorf("unrecognized type for module: %s", t)
}

Type guard

// Only tag fields whose type the loader supports
func isLoadableModuleField(t reflect.Type) bool {
    if t == reflect.TypeOf(caddy.ModuleMap{}) { return true }
    if t == reflect.TypeOf(json.RawMessage{}) { return true }
    if t.Kind() == reflect.Pointer { return true }
    return false
}

Try / catch

if _, err := ctx.LoadModule(fv, raw); err != nil { return fmt.Errorf("field %s: %w", field.Name, err) }

Prevention

When it happens

Trigger: A plugin declares a struct field with a caddy:"module=..." (or inline_key) tag whose type is not *SomeModule, ModuleMap, map[string]json.RawMessage, []json.RawMessage, or [][]json.RawMessage — e.g. an int, string, non-module struct, or non-pointer non-map type — and the module gets loaded during provisioning.

Common situations: Plugin authors copying module struct patterns and forgetting the field must be a pointer or ModuleMap; tagging a plain non-module field with a caddy module tag by accident; refactoring a field from *SomeModule to SomeModule (non-pointer).

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/b891f19ff37b7bde. Report an issue: GitHub.