symfony/http-kernel · error · RuntimeException
A restored response must have the X-Content-Digest header.
Error message
A restored response must have the X-Content-Digest header.
What it means
HttpCache Store::write persists a response; if the response already carries an X-Body-File header (meaning its body was previously restored from disk), it must also carry an X-Content-Digest header identifying the stored body. This RuntimeException is a safeguard against a corrupted or hand-crafted restored response being re-stored without its content digest, which would break cache consistency between the metadata file and the body file on disk.
Solutions
- Ensure any response carrying X-Body-File also carries the matching X-Content-Digest header before calling write()
- Never manually set or strip X-Body-File / X-Content-Digest headers; let Store::restore() create them
- If building a fresh Response to cache, remove the X-Body-File header so the store writes the body normally
Example fix
// before
$response->headers->set('X-Body-File', $path);
$store->write($request, $response);
// after
$response->headers->remove('X-Body-File'); // let the store recompute digest + body file
$store->write($request, $response); Defensive patterns
Strategy: validation
Validate before calling
if ($response->headers->has('X-Body-File') && !$response->headers->has('X-Content-Digest')) {
throw new \RuntimeException('Refusing to store: X-Body-File without X-Content-Digest');
}
$store->write($request, $response); Try / catch
try {
$store->write($request, $response);
} catch (\RuntimeException $e) {
$response->headers->remove('X-Body-File');
$response->headers->remove('X-Content-Digest');
$store->write($request, $response);
} Prevention
- Never manually set X-Body-File or X-Content-Digest headers
- Let Store::restore() create internal headers
- Subclass Store rather than editing cached Response objects directly
When it happens
Trigger: Calling HttpCache's Store::write() (via the HttpCache kernel) with a Response whose headers contain X-Body-File but not X-Content-Digest — e.g. custom code that manually sets X-Body-File to trick the store into skipping a body rewrite, or a subclass of Store that builds such responses.
Common situations: Custom HttpCache Store subclasses that copy/clone cached responses and strip internal headers; debugging code that mutates internal X-* headers; restoring a response manually from cache files with an incomplete header set.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- X-Body-File and X-Content-Digest do not match.
- Error when rendering
- Unable to process an ESI tag without a "src" attribute.
- Unable to process an SSI tag without a "virtual" attribute.
- Unable to create the store directory
AI-assisted analysis of symfony/http-kernel@aa3a39d728 (2026-09-13).
Data as JSON: /api/errors/17cd140aee13db88.
Report an issue: GitHub.
Appendix: source
Thrown at HttpCache/Store.php:186
}
/**
* Writes a cache entry to the store for the given Request and Response.
*
* Existing entries are read and any that match the response are removed. This
* method calls write with the new list of cache entries.
*
* @throws \RuntimeException
*/
public function write(Request $request, Response $response): string
{
$key = $this->getCacheKey($request);
$storedEnv = $this->persistRequest($request);
if ($response->headers->has('X-Body-File')) {
// Assume the response came from disk, but at least perform some safeguard checks
if (!$response->headers->has('X-Content-Digest')) {
throw new \RuntimeException('A restored response must have the X-Content-Digest header.');
}
$digest = $response->headers->get('X-Content-Digest');
if ($this->getPath($digest) !== $response->headers->get('X-Body-File')) {
throw new \RuntimeException('X-Body-File and X-Content-Digest do not match.');
}
// Everything seems ok, omit writing content to disk
} else {
// Responses that cannot provide their content, like BinaryFileResponse or
// StreamedResponse, have no entity to store, so no entry is written
if (false === $content = $response->getContent()) {
return $key;
}
$digest = $this->generateContentDigest($response);
$response->headers->set('X-Content-Digest', $digest);
if (!$this->save($digest, $content, false)) {View on GitHub (pinned to aa3a39d728)