caddyserver/caddy · error

decoded module is not a RequestMatcher or RequestMatcherWith

Error message

decoded module is not a RequestMatcher or RequestMatcherWithError: %#v

What it means

After modules decode from JSON, MatcherSets.FromInterface type-asserts each decoded value to RequestMatcher or RequestMatcherWithError. If a module loaded through the matcher namespace implements neither interface, this error is returned with the Go value printed. It signals a module registered under http.matchers.* that is not actually a matcher.

Source

Thrown at modules/caddyhttp/routes.go:453

		}
	}
	return len(ms) == 0, nil
}

// FromInterface fills ms from an 'any' value obtained from LoadModule.
func (ms *MatcherSets) FromInterface(matcherSets any) error {
	for _, matcherSetIfaces := range matcherSets.([]map[string]any) {
		var matcherSet MatcherSet
		for _, matcher := range matcherSetIfaces {
			if m, ok := matcher.(RequestMatcherWithError); ok {
				matcherSet = append(matcherSet, m)
				continue
			}
			if m, ok := matcher.(RequestMatcher); ok {
				matcherSet = append(matcherSet, m)
				continue
			}
			return fmt.Errorf("decoded module is not a RequestMatcher or RequestMatcherWithError: %#v", matcher)
		}
		*ms = append(*ms, matcherSet)
	}
	return nil
}

// TODO: Is this used?
func (ms MatcherSets) String() string {
	var result strings.Builder
	result.WriteByte('[')
	for _, matcherSet := range ms {
		for _, matcher := range matcherSet {
			fmt.Fprintf(&result, " %#v", matcher)
		}
	}
	result.WriteByte(']')
	return result.String()
}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. If you develop the plugin: make the type implement caddyhttp.RequestMatcher (Matches(r *http.Request) bool) or RequestMatcherWithError, and add a compile-time interface guard
  2. Verify the module ID namespace matches the module's actual role (http.matchers.* for matchers)
  3. If you only consume the config: remove the invalid module from the match block and report it to the plugin author

Example fix

// before (plugin)
func (m MyThing) CaddyModule() caddy.ModuleInfo {
  return caddy.ModuleInfo{ ID: "http.matchers.mything", New: func() caddy.Module { return new(MyThing) } }
}
// MyThing has no Matches method

// after
var _ caddyhttp.RequestMatcherWithError = (*MyThing)(nil)

func (m *MyThing) Matches(r *http.Request) error { return nil }
Defensive patterns

Strategy: type-guard

Type guard

// plugin-side compile-time guard (prevents shipping a matcher that isn't one):
var (
    _ caddyhttp.RequestMatcherWithError = (*MyMatcher)(nil)
    _ caddy.Module                      = (*MyMatcher)(nil)
)

// consumer-side runtime guard over decoded matchers:
func isRequestMatcher(v any) bool {
    switch v.(type) {
    case caddyhttp.RequestMatcher, caddyhttp.RequestMatcherWithError:
        return true
    }
    return false
}

Prevention

When it happens

Trigger: A custom plugin module whose CaddyModule() ID is under the matchers namespace but which does not implement RequestMatcherWithError/RequestMatcher; or a module mis-decoded into the match sets field via hand-crafted JSON.

Common situations: Writing a third-party Caddy plugin and getting the module ID namespace or interface implementation wrong; very rare with stock configs.

Related errors


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