symfony/http-kernel · error · RuntimeException

Unable to process an ESI tag without a "src" attribute.

Error message

Unable to process an ESI tag without a "src" attribute.

What it means

During ESI response processing, the response body is split on esi:include tags and their attributes parsed with regex (no XML parser, since ESI tags may appear in plain text). When a matched esi:include tag has no "src" attribute, there is no resource to fetch, so this validation guard fires. It indicates malformed ESI markup in the upstream response — an esi:include written without src (or with attributes the regex parser does not recognize).

Solutions

  1. Add a src attribute to every <esi:include> tag.
  2. Check templates for attribute typos (src, not href/url).
  3. Remove malformed esi tags if they are not intended to be processed.

Example fix

// before
<esi:include alt="fallback" />
// after
<esi:include src="/_fragment/news" alt="fallback" />
Defensive patterns

Strategy: validation

Validate before calling

if (str_contains($html, '<esi:include') && !preg_match('/<esi:include[^>]+src="[^"]+"/', $html)) { // fix templates }

Prevention

When it happens

Trigger: HTML content containing an <esi:include> tag lacking src=; process() is called on a text/html response containing such a tag.

Common situations: Hand-written or template-generated ESI tags with a typo'd attribute (href instead of src), or empty src stripped by templating.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of symfony/http-kernel@aa3a39d728 (2026-09-13). Data as JSON: /api/errors/84d115b8dde445a8. Report an issue: GitHub.

Appendix: source

Thrown at HttpCache/Esi.php:86

        // we don't use a proper XML parser here as we can have ESI tags in a plain text response
        $content = $response->getContent();
        $content = preg_replace('#<esi\:remove>.*?</esi\:remove>#s', '', $content);
        $content = preg_replace('#<esi\:comment[^>]+>#s', '', $content);

        $boundary = self::generateBodyEvalBoundary();
        $chunks = preg_split('#<esi\:include\s+(.*?)\s*(?:/|</esi\:include)>#', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);

        $i = 1;
        while (isset($chunks[$i])) {
            $options = [];
            preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $chunks[$i], $matches, \PREG_SET_ORDER);
            foreach ($matches as $set) {
                $options[$set[1]] = $set[2];
            }

            if (!isset($options['src'])) {
                throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.');
            }

            $chunks[$i] = $boundary.$options['src']."\n".($options['alt'] ?? '')."\n".('continue' === ($options['onerror'] ?? ''))."\n";
            $i += 2;
        }
        $content = $boundary.implode('', $chunks).$boundary;

        $response->setContent($content);
        $response->headers->set('X-Body-Eval', 'ESI');

        // remove ESI/1.0 from the Surrogate-Control header
        $this->removeFromControl($response);

        return $response;
    }
}

View on GitHub (pinned to aa3a39d728)