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

  1. Break the cycle: use {{ include "partial" }} (file include) instead of httpInclude for shared partials
  2. Trace the include chain (each hop logs the URI) and remove the edge that loops back
  3. Flatten deep chains to fewer than 3 levels of httpInclude
  4. 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

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


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