{"record":{"id":"84d115b8dde445a8","repo":"symfony/http-kernel","slug":"unable-to-process-an-esi-tag-without-a-src-attribute","errorCode":null,"errorMessage":"Unable to process an ESI tag without a \"src\" attribute.","messagePattern":"Unable to process an ESI tag without a \"src\" attribute\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"HttpCache/Esi.php","lineNumber":86,"sourceCode":"\n        // we don't use a proper XML parser here as we can have ESI tags in a plain text response\n        $content = $response->getContent();\n        $content = preg_replace('#<esi\\:remove>.*?</esi\\:remove>#s', '', $content);\n        $content = preg_replace('#<esi\\:comment[^>]+>#s', '', $content);\n\n        $boundary = self::generateBodyEvalBoundary();\n        $chunks = preg_split('#<esi\\:include\\s+(.*?)\\s*(?:/|</esi\\:include)>#', $content, -1, \\PREG_SPLIT_DELIM_CAPTURE);\n\n        $i = 1;\n        while (isset($chunks[$i])) {\n            $options = [];\n            preg_match_all('/(src|onerror|alt)=\"([^\"]*?)\"/', $chunks[$i], $matches, \\PREG_SET_ORDER);\n            foreach ($matches as $set) {\n                $options[$set[1]] = $set[2];\n            }\n\n            if (!isset($options['src'])) {\n                throw new \\RuntimeException('Unable to process an ESI tag without a \"src\" attribute.');\n            }\n\n            $chunks[$i] = $boundary.$options['src'].\"\\n\".($options['alt'] ?? '').\"\\n\".('continue' === ($options['onerror'] ?? '')).\"\\n\";\n            $i += 2;\n        }\n        $content = $boundary.implode('', $chunks).$boundary;\n\n        $response->setContent($content);\n        $response->headers->set('X-Body-Eval', 'ESI');\n\n        // remove ESI/1.0 from the Surrogate-Control header\n        $this->removeFromControl($response);\n\n        return $response;\n    }\n}\n","sourceCodeStart":68,"sourceCodeEnd":103,"githubUrl":"https://github.com/symfony/http-kernel/blob/aa3a39d7286a62cdfea98f0e69c651a3da6e36cf/HttpCache/Esi.php#L68-L103","documentation":"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).","triggerScenarios":"HTML content containing an <esi:include> tag lacking src=; process() is called on a text/html response containing such a tag.","commonSituations":"Hand-written or template-generated ESI tags with a typo'd attribute (href instead of src), or empty src stripped by templating.","solutions":["Add a src attribute to every <esi:include> tag.","Check templates for attribute typos (src, not href/url).","Remove malformed esi tags if they are not intended to be processed."],"exampleFix":"// before\n<esi:include alt=\"fallback\" />\n// after\n<esi:include src=\"/_fragment/news\" alt=\"fallback\" />","handlingStrategy":"validation","validationCode":"if (str_contains($html, '<esi:include') && !preg_match('/<esi:include[^>]+src=\"[^\"]+\"/', $html)) { // fix templates }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Template lint for ESI tags requiring src","Use a Twig helper to generate esi tags","Add integration tests rendering pages with ESI includes"],"tags":["php","symfony","esi","http-cache","html"],"backgroundTag":"missing-required-argument","analyzedSha":"aa3a39d7286a62cdfea98f0e69c651a3da6e36cf","analyzedAt":"2026-09-13T18:03:36.509Z","contentChangedAt":"2026-09-13T18:03:36.509Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}