getgrav/grav · error · RuntimeException
User account with this username already exists
Error message
User account with this username already exists
What it means
When saving a new DataUser account that has no target file yet, User::save() resolves account://<username>.yaml through the locator (line 142); if that file already exists it throws instead of silently overwriting. This is creation-time collision protection for file-based accounts: an existing account's data cannot be clobbered by a fresh save.
Source
Thrown at system/src/Grav/Common/User/DataUser/User.php:142
if (!$file || !$file->filename()) {
user_error(self::class . ': calling \$user = new ' . self::class . "() is deprecated since Grav 1.6, use \$grav['accounts']->load(\$username) or \$grav['accounts']->load('') instead", E_USER_DEPRECATED);
}
if ($file) {
$username = $this->filterUsername((string)$this->get('username'));
// Validate username to prevent path traversal attacks
if (!self::isValidUsername($username)) {
throw new \RuntimeException('Invalid username: contains invalid characters or sequences');
}
if (!$file->filename()) {
$locator = Grav::instance()['locator'];
// Check if a user with this username already exists (prevent overwriting)
$existingFile = $locator->findResource('account://' . $username . YAML_EXT);
if ($existingFile) {
throw new \RuntimeException('User account with this username already exists');
}
$file->filename($locator->findResource('account://' . $username . YAML_EXT, true, true));
}
// if plain text password, hash it and remove plain text
$password = $this->get('password') ?? $this->get('password1');
if (null !== $password && '' !== $password) {
$password2 = $this->get('password2');
if (!\is_string($password) || ($password2 && $password !== $password2)) {
throw new \RuntimeException('Passwords did not match.');
}
$this->set('hashed_password', Authentication::create($password));
}
$this->undef('password');
$this->undef('password1');
$this->undef('password2');View on GitHub (pinned to 6040efed04)
Solutions
- Check existence first: if the locator finds account://<username>.yaml (or $grav['accounts'] reports the user), reject the submission as 'username taken'.
- Make registration handlers idempotent: on this error, treat the account as created and log the user in rather than retrying the insert.
- For migrations, pre-scan the accounts directory and skip/rename duplicates before saving.
Example fix
// before
$user = $grav['accounts']->load('');
$user->set('username', $username);
$user->merge($data);
$user->save(); // throws: account/<username>.yaml exists
// after
if ($grav['locator']->findResource('account://' . $username . '.yaml')) {
throw new \DomainException('Username is already taken.');
}
$user = $grav['accounts']->load('');
$user->set('username', $username);
$user->merge($data);
$user->save(); Defensive patterns
Strategy: validation
Validate before calling
$locator = $grav['locator'];
if ($locator->findResource('account://' . $username . '.yaml') !== false) {
// reject: username already taken
} Type guard
function isUsernameAvailable(\Grav\Common\Grav $grav, string $username): bool
{
return false === $grav['locator']->findResource('account://' . $username . '.yaml');
} Try / catch
try {
$user->save();
} catch (\RuntimeException $e) {
if (str_contains($e->getMessage(), 'already exists')) {
// treat as 'username taken': re-show registration form
}
throw $e;
} Prevention
- Check account existence before every create; treat the exception as a race-condition backstop.
- Make registration handlers idempotent (double submit must not 500).
- Use case-insensitive duplicate checks if your filesystem is case-insensitive.
When it happens
Trigger: Calling save() on an account object created with $grav['accounts']->load('') while account/<username>.yaml already exists; double submission of a registration form; re-running an import/migration that partially completed.
Common situations: Registration race or double-click on submit; non-idempotent migration scripts; case-sensitivity surprises where 'User1' collides with 'user1' on case-insensitive filesystems.
Related errors
- Passwords did not match.
- Invalid username: contains invalid characters or sequences
- Passwords did not match.
- %s: Relationship %s does not exist
- 400
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/bcad6ce9294395a5.
Report an issue: GitHub.