getgrav/grav · error · RuntimeException
Failed to save file {filepath}
Error message
Failed to save file {filepath} What it means
In AbstractFile::save(), when the target path is a symlink the code resolves it with realpath() before the atomic temp-file write; realpath() returns false for a symlink whose target does not exist, and that branch throws this RuntimeException immediately — no data is written. So this particular 'Failed to save file' means exactly one thing: a dangling symlink at the save path.
Source
Thrown at system/src/Grav/Framework/File/AbstractFile.php:296
$dir = $this->getPath();
if (!$this->mkdir($dir)) {
throw new RuntimeException('Creating directory failed for ' . $filepath);
}
try {
if ($this->handle) {
$tmp = true;
// As we are using non-truncating locking, make sure that the file is empty before writing.
if (@ftruncate($this->handle, 0) === false || @fwrite($this->handle, (string) $data) === false) {
// Writing file failed, throw an error.
$tmp = false;
}
} else {
// Support for symlinks.
$realpath = is_link($filepath) ? realpath($filepath) : $filepath;
if ($realpath === false) {
throw new RuntimeException('Failed to save file ' . $filepath);
}
// Create file with a temporary name and rename it to make the save action atomic.
$tmp = $this->tempname($realpath);
if (@file_put_contents($tmp, $data) === false) {
$tmp = false;
} elseif (@rename($tmp, $realpath) === false) {
@unlink($tmp);
$tmp = false;
}
}
} catch (Exception) {
$tmp = false;
}
if ($tmp === false) {
throw new RuntimeException('Failed to save file ' . $filepath);
}View on GitHub (pinned to 6040efed04)
Solutions
- Inspect the link: ls -l <path> and readlink -f <path> — an empty/failed resolution confirms the dangling target.
- Recreate the symlink to a valid target: ln -sfn /real/target <path>.
- Or remove the symlink so save() creates a regular file: rm <path> (only if a real file is intended).
- If deploys swap targets behind links, resolve the real path once at configuration time and save there directly.
Example fix
# before: RuntimeException "Failed to save file /srv/grav/current/data.json" (path is a symlink) readlink -f /srv/grav/current/data.json # empty -> dangling ln -sfn /srv/grav/shared/data.json /srv/grav/current/data.json # after: save() resolves the link and writes the real target atomically
Defensive patterns
Strategy: validation
Validate before calling
$path = $file->getFilePath();
if (is_link($path) && realpath($path) === false) {
// dangling symlink: recreate the link or point storage elsewhere before save()
} Try / catch
try {
$file->save($data);
} catch (\RuntimeException $e) {
if (is_link($path) && realpath($path) === false) {
unlink($path); // then retry save(), or relink to a valid target
}
} Prevention
- Validate symlink targets during deploy smoke tests (readlink -f must resolve).
- Store resolved real paths in configuration instead of routing writes through symlinks.
- Alert when shared volumes backing symlinked data are unmounted.
When it happens
Trigger: save() where filepath is a symlink to a deleted or moved target; deploy pipelines that atomically swap directories behind symlinks; a relative symlink that no longer resolves after the installation moved or a mount changed; a symlink chain with a broken intermediate link.
Common situations: user/data or cache files symlinked to a shared volume that got unmounted; capistrano-style releases directories where the old release was removed; symlinks created on one host and used on another with different absolute paths.
Related errors
- Invalid backup location: {$backup_root}
- Creating directory failed for {filepath}
- Opening file for writing failed on error {$message}
- Bad Data
- Failed to save file '%s': %s
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/feb93541c719d869.
Report an issue: GitHub.