caddyserver/caddy · error
virtual request cycle
Error message
virtual request cycle
What it means
Returned by the templates module's httpInclude function when the recursion-prevention header indicates the virtual request is already at nesting depth 3 or greater. Caddy increments this header on each nested virtual include to bound recursion.
Source
Thrown at modules/caddyhttp/templates/tplcontext.go:183
return nil
}
// funcHTTPInclude returns the body of a virtual (lightweight) request
// to the given URI on the same server. Note that included bodies
// are NOT escaped, so you should only include trusted resources.
// If it is not trusted, be sure to use escaping functions yourself.
func (c TemplateContext) funcHTTPInclude(uri string) (string, error) {
// prevent virtual request loops by counting how many levels
// deep we are; and if we get too deep, return an error
recursionCount := 1
if numStr := c.Req.Header.Get(recursionPreventionHeader); numStr != "" {
num, err := strconv.Atoi(numStr)
if err != nil {
return "", fmt.Errorf("parsing %s: %v", recursionPreventionHeader, err)
}
if num >= 3 {
return "", fmt.Errorf("virtual request cycle")
}
recursionCount = num + 1
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
virtReq, err := http.NewRequest("GET", uri, nil)
if err != nil {
return "", err
}
virtReq.Host = c.Req.Host
virtReq.RemoteAddr = "127.0.0.1:10000" // https://github.com/caddyserver/caddy/issues/5835
virtReq.Header = c.Req.Header.Clone()
virtReq.Header.Set("Accept-Encoding", "identity") // https://github.com/caddyserver/caddy/issues/4352
virtReq.Trailer = c.Req.Trailer.Clone()
virtReq.Header.Set(recursionPreventionHeader, strconv.Itoa(recursionCount))View on GitHub (pinned to 50e54ee279)
Solutions
- Break the cycle: use {{ include "partial" }} (file include) instead of httpInclude for shared partials
- Trace the include chain (each hop logs the URI) and remove the edge that loops back
- Flatten deep chains to fewer than 3 levels of httpInclude
- Ensure error pages that use templates do not httpInclude a URL that errors and re-enters the error handler
Example fix
<!-- before -->
<!-- a.html -->
{{ httpInclude "/b" }}
<!-- b.html -->
{{ httpInclude "/a" }}
<!-- after -->
<!-- a.html -->
{{ include "/b.html" }}
<!-- b.html -->
(no include back; render content directly) Defensive patterns
Strategy: fallback
Prevention
- Prefer file includes ({{ include }}) over httpInclude for shared partials
- Document the include graph for each site to avoid cycles
- Keep httpInclude chains under 3 levels by design
When it happens
Trigger: A template that calls {{ httpInclude "/x" }} where /x itself renders a template that httpIncludes back into /x (a cycle), or a legitimate include chain deeper than 3 levels. The header value reaching >= 3 triggers the error.
Common situations: Two templates including each other (A includes B, B includes A), a template including its own URL, or a shared header partial fetched via httpInclude that indirectly includes itself.
Related errors
- parsing %s: %v
- http %d
- unterminated front matter
- loading template extensions: %v
- delimiters must consist of exactly two elements: opening and
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/28575ce703f807f7.
Report an issue: GitHub.