{"record":{"id":"feb93541c719d869","repo":"getgrav/grav","slug":"failed-to-save-file-filepath","errorCode":null,"errorMessage":"Failed to save file {filepath}","messagePattern":"Failed to save file (.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Framework/File/AbstractFile.php","lineNumber":296,"sourceCode":"        $dir = $this->getPath();\n\n        if (!$this->mkdir($dir)) {\n            throw new RuntimeException('Creating directory failed for ' . $filepath);\n        }\n\n        try {\n            if ($this->handle) {\n                $tmp = true;\n                // As we are using non-truncating locking, make sure that the file is empty before writing.\n                if (@ftruncate($this->handle, 0) === false || @fwrite($this->handle, (string) $data) === false) {\n                    // Writing file failed, throw an error.\n                    $tmp = false;\n                }\n            } else {\n                // Support for symlinks.\n                $realpath = is_link($filepath) ? realpath($filepath) : $filepath;\n                if ($realpath === false) {\n                    throw new RuntimeException('Failed to save file ' . $filepath);\n                }\n\n                // Create file with a temporary name and rename it to make the save action atomic.\n                $tmp = $this->tempname($realpath);\n                if (@file_put_contents($tmp, $data) === false) {\n                    $tmp = false;\n                } elseif (@rename($tmp, $realpath) === false) {\n                    @unlink($tmp);\n                    $tmp = false;\n                }\n            }\n        } catch (Exception) {\n            $tmp = false;\n        }\n\n        if ($tmp === false) {\n            throw new RuntimeException('Failed to save file ' . $filepath);\n        }","sourceCodeStart":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Framework/File/AbstractFile.php#L278-L314","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before: RuntimeException \"Failed to save file /srv/grav/current/data.json\" (path is a symlink)\nreadlink -f /srv/grav/current/data.json    # empty -> dangling\nln -sfn /srv/grav/shared/data.json /srv/grav/current/data.json\n# after: save() resolves the link and writes the real target atomically","handlingStrategy":"validation","validationCode":"$path = $file->getFilePath();\nif (is_link($path) && realpath($path) === false) {\n    // dangling symlink: recreate the link or point storage elsewhere before save()\n}","typeGuard":null,"tryCatchPattern":"try {\n    $file->save($data);\n} catch (\\RuntimeException $e) {\n    if (is_link($path) && realpath($path) === false) {\n        unlink($path); // then retry save(), or relink to a valid target\n    }\n}","preventionTips":["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."],"tags":["php","grav","filesystem","symlink","file-save","atomic-write"],"backgroundTag":"dangling-symlink","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}