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

  1. Check existence first: if the locator finds account://<username>.yaml (or $grav['accounts'] reports the user), reject the submission as 'username taken'.
  2. Make registration handlers idempotent: on this error, treat the account as created and log the user in rather than retrying the insert.
  3. 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

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


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