getgrav/grav · error · InvalidArgumentException

Cache key "%s" contains reserved characters {}()/\@:

Error message

Cache key "%s" contains reserved characters {}()/\@:

What it means

PSR-16 reserves the characters {}()/\@: in cache keys because several backends use them as delimiters or namespace separators. Grav's validateKey() checks with strpbrk() and throws InvalidArgumentException naming the offending key when any reserved character is present.

Source

Thrown at system/src/Grav/Framework/Cache/CacheTrait.php:322

    {
        if (!is_string($key)) {
            throw new InvalidArgumentException(
                sprintf(
                    'Cache key must be string, "%s" given',
                    get_debug_type($key)
                )
            );
        }
        if (!isset($key[0])) {
            throw new InvalidArgumentException('Cache key length must be greater than zero');
        }
        if (strlen($key) > 64) {
            throw new InvalidArgumentException(
                sprintf('Cache key length must be less than 65 characters, key had %d characters', strlen($key))
            );
        }
        if (strpbrk($key, '{}()/\@:') !== false) {
            throw new InvalidArgumentException(
                sprintf('Cache key "%s" contains reserved characters {}()/\@:', $key)
            );
        }
    }

    /**
     * @param array $keys
     * @return void
     * @throws InvalidArgumentException
     */
    protected function validateKeys(iterable $keys): void
    {
        if (!$this->validation) {
            return;
        }

        foreach ($keys as $key) {
            $this->validateKey($key);

View on GitHub (pinned to 6040efed04)

Solutions

  1. Hash or encode the natural key: $key = md5($uri) or $key = urlencode($path) — but prefer hashing since urlencode can still exceed 64 chars.
  2. Replace delimiters when a readable key matters: $key = strtr($path, ['/' => '-', ':' => '_']).
  3. Wrap key generation once (single KeyBuilder/helper) so no call site hand-crafts keys from URLs or paths.

Example fix

// before
$cache->get('page:' . $uri); // 'page:https://...' -> reserved : / chars -> throws

// after
$cache->get('page-' . md5($uri)); // reserved-free
Defensive patterns

Strategy: validation

Validate before calling

// make any natural key safe before use
$safeKey = preg_replace('/[{}()\/\\@:]/', '-', $naturalKey) ?? '';
if (strlen($safeKey) > 64) {
    $safeKey = 'k-' . md5($naturalKey);
}
$cache->get($safeKey !== '' ? $safeKey : 'k-default');

Prevention

When it happens

Trigger: Using a URL as a key ('https://example.com/page' contains '/', ':' and '//'); using file paths ('user/pages/blog.md' contains '/'); keys templated with sprintf braces ('page_{id}'); email addresses or user@host identifiers ('@'); Windows-style paths ('\').

Common situations: Keying page-fragment caches by raw REQUEST_URI; caching by filesystem path in custom plugins; interpolating templates with literal braces in the key; multi-site setups embedding 'site(a)' style markers in keys.

Related errors


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