caddyserver/caddy · error

a cycle of imports exists between %s and %s

Error message

a cycle of imports exists between %s and %s

What it means

The import graph detects that adding an edge from -> to would create a cycle: to (directly or transitively) already imports from, so making 'from' import 'to' would recurse forever. Caddy refuses the edge instead of looping.

Source

Thrown at caddyconfig/caddyfile/importgraph.go:61

}

func (i *importGraph) removeNode(name string) {
	delete(i.nodes, name)
}

func (i *importGraph) removeNodes(names []string) {
	for _, name := range names {
		i.removeNode(name)
	}
}

func (i *importGraph) addEdge(from, to string) error {
	if !i.exists(from) || !i.exists(to) {
		return fmt.Errorf("one of the nodes does not exist")
	}

	if i.willCycle(to, from) {
		return fmt.Errorf("a cycle of imports exists between %s and %s", from, to)
	}

	if i.areConnected(from, to) {
		// if connected, there's nothing to do
		return nil
	}

	if i.nodes == nil {
		i.nodes = make(map[string]struct{})
	}
	if i.edges == nil {
		i.edges = make(adjacency)
	}

	i.edges[from] = append(i.edges[from], to)
	return nil
}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Break the cycle: move the shared directives both files need into a third snippet that neither imports the other
  2. Trace the chain between the two named files to find the smallest cycle
  3. Use parameterized snippets (import with args) instead of files importing each other for reuse
  4. Lint imports with `caddy adapt` after each refactor to catch cycles early

Example fix

# before: a.caddy does 'import b.caddy', b.caddy does 'import a.caddy'
# after: extract shared parts into common.caddy; a.caddy and b.caddy each 'import common.caddy'
Defensive patterns

Strategy: validation

Validate before calling

// pre-flight: build the import graph yourself and reject cycles before adapting
seen := map[string]bool{}
var visit func(path string, stack []string) error
visit = func(path string, stack []string) error {
    if seen[path] { return nil }
    for _, s := range stack { if s == path { return fmt.Errorf("cycle via %s", path) } }
    // recurse into import statements found in path...
    return nil
}

Prevention

When it happens

Trigger: Two or more files/snippets import each other, directly or through intermediaries: A imports B, then B (or a file B imported) imports A. Detected in importgraph.addEdge during Caddyfile adaptation.

Common situations: Refactoring shared snippets where a common file grows to import a site file that imports it back; mutual imports between 'defaults' and 'overrides' snippets. The message names the two nodes that would close the cycle.

Related errors


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