caddyserver/caddy · critical

malformed tag on field %s: %v

Error message

malformed tag on field %s: %v

What it means

Caddy's Context.LoadModule reads the `caddy:"..."` struct tag on a module field to learn how to load nested modules. ParseStructTag failed to parse that tag, which is a programming error in the module's source code, not a runtime/config problem. Because this is a panic, it crashes the process at provisioning time when the field is loaded.

Source

Thrown at context.go:199

// to type-assert each 'any' value(s) to the types that are useful to you
// and store them on the same struct. Storing them on the same struct makes for
// easy garbage collection when your host module is no longer needed.
//
// Loaded modules have already been provisioned and validated. Upon returning
// successfully, this method clears the json.RawMessage(s) in the field since
// the raw JSON is no longer needed, and this allows the GC to free up memory.
func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error) {
	val := reflect.ValueOf(structPointer).Elem().FieldByName(fieldName)
	typ := val.Type()

	field, ok := reflect.TypeOf(structPointer).Elem().FieldByName(fieldName)
	if !ok {
		panic(fmt.Sprintf("field %s does not exist in %#v", fieldName, structPointer))
	}

	opts, err := ParseStructTag(field.Tag.Get("caddy"))
	if err != nil {
		panic(fmt.Sprintf("malformed tag on field %s: %v", fieldName, err))
	}

	moduleNamespace, ok := opts["namespace"]
	if !ok {
		panic(fmt.Sprintf("missing 'namespace' key in struct tag on field %s", fieldName))
	}
	inlineModuleKey := opts["inline_key"]

	var result any

	switch val.Kind() {
	case reflect.Slice:
		if isJSONRawMessage(typ) {
			// val is `json.RawMessage` ([]uint8 under the hood)

			if inlineModuleKey == "" {
				panic("unable to determine module name without inline_key when type is not a ModuleMap")
			}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Fix the struct tag so it is valid key=value pairs: `caddy:"namespace=http.handlers"` or `caddy:"namespace=http.handlers inline_key=handler"`
  2. Verify the tag string with go vet / structtag linters which catch malformed tags at compile time
  3. Confirm the tag is on the exact field name passed to LoadModule

Example fix

// before
type Handler struct {
    Upstream *json.RawMessage `json:"upstream" caddy:"namespace=http.reverse_proxy.upstreams"` // missing closing quote in tag content
}
// after
type Handler struct {
    Upstream *json.RawMessage `json:"upstream" caddy:"namespace=http.reverse_proxy.upstreams"`
}
Defensive patterns

Strategy: validation

Validate before calling

// before LoadModule, parse the tag yourself to fail fast with a clear error
import "reflect"

func validateCaddyTag(ptr any, fieldName string) error {
    f, ok := reflect.TypeOf(ptr).Elem().FieldByName(fieldName)
    if !ok {
        return fmt.Errorf("field %s missing", fieldName)
    }
    if _, err := caddy.ParseStructTag(f.Tag.Get("caddy")); err != nil {
        return fmt.Errorf("bad tag on %s: %w", fieldName, err)
    }
    return nil
}

Try / catch

// last resort in module plumbing: wrap provisioning in a recover
func safeLoad(ctx caddy.Context, ptr any, field string) (m any, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("LoadModule panic: %v", r)
        }
    }()
    return ctx.LoadModule(ptr, field)
}

Prevention

When it happens

Trigger: Calling ctx.LoadModule(&handler, "Field") where Field's struct tag is malformed, e.g. `caddy:"namespace=http.handlers"` (missing value), unbalanced quotes, or a tag not made of key=value pairs.

Common situations: Writing a custom Caddy module and hand-typing the caddy struct tag; copy-pasting a tag from another field and breaking the syntax; typos like `caddy:"namespace=http.handlers inline_key=module"` with stray characters.

Understand the failure class

Related errors


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