caddyserver/caddy · error
, at : import chain [' ']
Error message
%w, at %s:%d import chain ['%s']
What it means
WrapErr decorates any Caddyfile parse error with the file and line of the current token; this variant fires when the token came from an imported file (via import), appending the full import chain so you can trace which snippet imported the failing line.
Solutions
- Open the file:line in the message; the import chain after 'import chain' lists the hops from the main Caddyfile
- Fix the underlying error described before the ', at file:line' part
- If the chain is confusing, temporarily inline the imported file to get a flat error
- Check for stale imported files that reference directives removed in your Caddy version
Example fix
# before: snippets/site.caddy has a typo root * /vr-srv # inside import snippets/*.caddy # after root * /var/www
Defensive patterns
Strategy: try-catch
Try / catch
if err := serverType.Setup(serverBlocks); err != nil {
var line, file string
if _, e := fmt.Sscanf(err.Error(), "%s at %s", &line, &file); e == nil {
// surface file/line and import chain to the user in CI output
}
} Prevention
- Run `caddy adapt` in CI to catch snippet errors before deploy
- Keep import trees shallow; prefer one level of snippets
- Version-control imported snippet files alongside the main Caddyfile
When it happens
Trigger: Any Caddyfile error constructed with d.Err/d.Errf/d.WrapErr where the current token has a non-empty imports list — i.e. the error is inside a file or snippet reached through one or more import statements.
Common situations: A syntax error or unrecognized directive inside an imported snippet (e.g. site snippets in a separate file pulled in with import blocks/*.caddy). The import chain names each hop, so files deep in an import tree are debuggable.
Related errors
- , at
- a cycle of imports exists between
- cannot use duplicate server name
- acme_dns specified without DNS provider config, but no…
- adapting config using
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/7ffd6529bd38364e.
Report an issue: GitHub.
Appendix: source
Thrown at caddyconfig/caddyfile/dispenser.go:445
// the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error {
return d.Errf("unexpected EOF")
}
// Err generates a custom parse-time error with a message of msg.
func (d *Dispenser) Err(msg string) error {
return d.WrapErr(errors.New(msg))
}
// Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...any) error {
return d.WrapErr(fmt.Errorf(format, args...))
}
// WrapErr takes an existing error and adds the Caddyfile file and line number.
func (d *Dispenser) WrapErr(err error) error {
if len(d.Token().imports) > 0 {
return fmt.Errorf("%w, at %s:%d import chain ['%s']", err, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
}
return fmt.Errorf("%w, at %s:%d", err, d.File(), d.Line())
}
// Delete deletes the current token and returns the updated slice
// of tokens. The cursor is not advanced to the next token.
// Because deletion modifies the underlying slice, this method
// should only be called if you have access to the original slice
// of tokens and/or are using the slice of tokens outside this
// Dispenser instance. If you do not re-assign the slice with the
// return value of this method, inconsistencies in the token
// array will become apparent (or worse, hide from you like they
// did me for 3 and a half freaking hours late one night).
func (d *Dispenser) Delete() []Token {
if d.cursor >= 0 && d.cursor <= len(d.tokens)-1 {
d.tokens = append(d.tokens[:d.cursor], d.tokens[d.cursor+1:]...)
d.cursor--
}View on GitHub (pinned to 50e54ee279)