{"record":{"id":"5a14dadea07c712d","repo":"getgrav/grav","slug":"cache-key-s-contains-reserved-characters","errorCode":null,"errorMessage":"Cache key \"%s\" contains reserved characters {}()/\\@:","messagePattern":"Cache key \"(.+?)\" contains reserved characters (.+?)\\(\\)/\\\\@:","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Framework/Cache/CacheTrait.php","lineNumber":322,"sourceCode":"    {\n        if (!is_string($key)) {\n            throw new InvalidArgumentException(\n                sprintf(\n                    'Cache key must be string, \"%s\" given',\n                    get_debug_type($key)\n                )\n            );\n        }\n        if (!isset($key[0])) {\n            throw new InvalidArgumentException('Cache key length must be greater than zero');\n        }\n        if (strlen($key) > 64) {\n            throw new InvalidArgumentException(\n                sprintf('Cache key length must be less than 65 characters, key had %d characters', strlen($key))\n            );\n        }\n        if (strpbrk($key, '{}()/\\@:') !== false) {\n            throw new InvalidArgumentException(\n                sprintf('Cache key \"%s\" contains reserved characters {}()/\\@:', $key)\n            );\n        }\n    }\n\n    /**\n     * @param array $keys\n     * @return void\n     * @throws InvalidArgumentException\n     */\n    protected function validateKeys(iterable $keys): void\n    {\n        if (!$this->validation) {\n            return;\n        }\n\n        foreach ($keys as $key) {\n            $this->validateKey($key);","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Framework/Cache/CacheTrait.php#L304-L340","documentation":"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.","triggerScenarios":"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 ('\\').","commonSituations":"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.","solutions":["Hash or encode the natural key: $key = md5($uri) or $key = urlencode($path) — but prefer hashing since urlencode can still exceed 64 chars.","Replace delimiters when a readable key matters: $key = strtr($path, ['/' => '-', ':' => '_']).","Wrap key generation once (single KeyBuilder/helper) so no call site hand-crafts keys from URLs or paths."],"exampleFix":"// before\n$cache->get('page:' . $uri); // 'page:https://...' -> reserved : / chars -> throws\n\n// after\n$cache->get('page-' . md5($uri)); // reserved-free","handlingStrategy":"validation","validationCode":"// make any natural key safe before use\n$safeKey = preg_replace('/[{}()\\/\\\\@:]/', '-', $naturalKey) ?? '';\nif (strlen($safeKey) > 64) {\n    $safeKey = 'k-' . md5($naturalKey);\n}\n$cache->get($safeKey !== '' ? $safeKey : 'k-default');","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never use raw URLs, file paths, or emails as cache keys — hash or strtr() them first.","Avoid sprintf templates with literal braces ('{id}') in key strings.","One shared key-sanitizing helper beats per-call-site improvisation."],"tags":["grav","cache","psr-16","invalid-key","reserved-characters"],"backgroundTag":"invalid-cache-key","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}