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
- Break the cycle: move the shared directives both files need into a third snippet that neither imports the other
- Trace the chain between the two named files to find the smallest cycle
- Use parameterized snippets (import with args) instead of files importing each other for reuse
- 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
- Make snippet files leaf nodes: snippets import nothing
- Refactor shared config into a single common snippet
- Adapt configs in CI to catch cycles introduced by refactors
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
- %w, at %s:%d import chain ['%s']
- consolidating TLS connection policies for server %d: %v
- applying global server options: %v
- cannot use duplicate server name '%s'
- could not parse octal permission bits in %s: %v
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/8d4b39db4339edb3.
Report an issue: GitHub.