getgrav/grav · error · RuntimeException
Unable to create directory: %s
Error message
Unable to create directory: %s
What it means
Folder::create() (alias mkdir) wraps PHP mkdir($folder, 0777, true); if the recursive creation fails and a re-check (with clearstatcache) still shows no directory, it throws RuntimeException 'Unable to create directory: /path'. The double-check exists because mkdir can report failure while the directory actually exists (race); the throw therefore means the directory genuinely could not be created — permissions, open_basedir, path collision with an existing file, or disk exhaustion.
Source
Thrown at system/src/Grav/Common/Filesystem/Folder.php:476
/**
* @param string $folder
* @return void
* @throws RuntimeException
*/
public static function create($folder)
{
// Silence error for open_basedir; should fail in mkdir instead.
if (@is_dir($folder)) {
return;
}
$success = @mkdir($folder, 0777, true);
if (!$success) {
// Take yet another look, make sure that the folder doesn't exist.
clearstatcache(true, $folder);
if (!@is_dir($folder)) {
throw new RuntimeException(sprintf('Unable to create directory: %s', $folder));
}
}
}
/**
* Recursive copy of one directory to another
*
* @param string $src
* @param string $dest
* @return bool
* @throws RuntimeException
*/
public static function rcopy($src, $dest, $preservePermissions = false)
{
// If the src is not a directory do a simple file copy
if (!is_dir($src)) {
copy($src, $dest);View on GitHub (pinned to 6040efed04)
Solutions
- Check write permission on the deepest existing ancestor: ls -ld $(dirname $folder) and compare with the PHP process user (whoami / posix_geteuid)
- chown/chmod the parent chain so the web user can create children (e.g. chown -R www-data cache/ && chmod 755 cache/)
- Verify open_basedir covers every path segment: php -i | grep open_basedir
- Confirm nothing occupies the path as a file (ls -la $folder) and that the volume is not read-only/full (df -h, df -i)
Example fix
// before
Folder::create(GRAV_ROOT . '/cache/compiled/pages'); // may throw blind
// after — fail with context before calling
use Grav\Common\Filesystem\Folder;
$dir = GRAV_ROOT . '/cache/compiled/pages';
$parent = dirname($dir);
if (!is_dir($dir) && !is_writable($parent)) {
throw new RuntimeException(sprintf('Cannot create %s: parent %s not writable by UID %d', $dir, $parent, posix_geteuid()));
}
Folder::create($dir); Defensive patterns
Strategy: validation
Validate before calling
if (!is_dir($folder)) {
$parent = dirname($folder);
if (!is_dir($parent) || !is_writable($parent)) {
throw new RuntimeException(sprintf('%s is not writable by UID %d — cannot create %s', $parent, posix_geteuid(), $folder));
}
}
Folder::create($folder); Try / catch
try {
Folder::create($folder);
} catch (RuntimeException $e) {
// check open_basedir, existing file at $folder, disk space, parent perms
$log->error($e->getMessage());
throw;
} Prevention
- Verify is_writable() on the parent before deep creates; open_basedir failures are silent until mkdir
- Ensure no regular file shadows the intended directory path
- Include writable-directory assertions in deployment smoke tests (cache/, user/pages write checks)
When it happens
Trigger: Parent directory not writable by the PHP user (cache/, user/ under www-data while PHP runs as another UID); open_basedir excluding an ancestor of $folder; a regular file already exists at the exact $folder path; disk full or inode exhaustion; read-only filesystem mounts.
Common situations: Fresh installs where storage/ or cache/ were not chowned to the web user; hardening php.ini tightening open_basedir after the site worked; deploying with a Docker volume mounted read-only; permission-fix scripts that created files where directories were expected.
Related errors
- Creating directory failed for {filepath}
- Unable to %s: %s
- Opening file for writing failed on error {$message}
- Invalid backup location: {$backup_root}
- Unknown error
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/c59876f00b126271.
Report an issue: GitHub.