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

  1. Inspect the link: ls -l <path> and readlink -f <path> — an empty/failed resolution confirms the dangling target.
  2. Recreate the symlink to a valid target: ln -sfn /real/target <path>.
  3. Or remove the symlink so save() creates a regular file: rm <path> (only if a real file is intended).
  4. 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

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


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/feb93541c719d869. Report an issue: GitHub.