skip to content

Under PSR-6 and PSR-16, which cache key characters and lengths must every implementation accept, and which characters are reserved?

level: middleimportance: nice to knowfreq 18%

answer

  1. A-Z, a-z, 0-9, underscore, period
  2. at least 64 characters, UTF-8
  3. reserved: {}()/\@:
  4. illegal key: InvalidArgumentException
  5. hash long or user-supplied parts

basics

~20 s

Both PSRs require implementations to accept keys of A-Z, a-z, 0-9, underscore and period, up to 64 characters in UTF-8. The characters {}()/@: are reserved and must not be supported; an illegal key throws the standard's InvalidArgumentException.

solid answer

~40 s

PSR-16 copies PSR-6's key definition: a key is a string of at least one character, and every implementation must support keys made of `A-Z`, `a-z`, `0-9`, `_` and `.` in any order, in UTF-8, up to 64 characters. Implementations may accept more characters or longer keys, but portable code cannot rely on that. The characters `{}()/\@:` are reserved for future extensions and implementations must not support them. Passing an illegal key makes `getItem()`, `get()`, `set()` and friends throw `Psr\Cache\InvalidArgumentException` or `Psr\SimpleCache\InvalidArgumentException`. So a library should never build keys straight from URLs, e-mail addresses or namespaced class names: prefix a short fixed name and hash the variable part, for example `'rate.' . hash('xxh128', $url)`, which stays well under 64 characters.

code

php · 11 lines
php
<?php
declare(strict_types=1);

function rateCacheKey(string $sourceUrl, string $pair): string
{
    // 'rate.' + 32 hex chars + '.' + 6 letters = 44 characters, all legal
    return 'rate.' . hash('xxh128', $sourceUrl) . '.' . preg_replace('/[^A-Z]/', '', strtoupper($pair));
}

echo rateCacheKey('https://rates.example/v1', 'eur/usd');
// rate.<32 hex characters>.EURUSD

go deeper

for a junior

Remember the safe set, letters, digits, underscore and period, up to 64 characters, and that : / \ @ and brackets are off limits.

for a middle

Explain which exception an illegal key raises in PSR-6 and PSR-16, and why implementations that accept more characters still make such keys non-portable.

for a senior

Design a key builder for a reusable library that hashes variable parts, stays under 64 characters, and is covered by tests with hostile inputs.

for a principal

Decide whether a codebase standardises one key-building helper for every package, trading a small shared dependency for consistent, portable keys.

## The shared key definition **PSR-6** defines a cache key, and **PSR-16** copies the definition word for word. A key is a string of at least one character that uniquely identifies a cached item, and: - implementations **must** support keys made of `A-Z`, `a-z`, `0-9`, `_` and `.`, in any order, in UTF-8, with a length of **up to 64 characters**; - implementations **may** support more characters, other encodings or longer keys, but at least that minimum; - implementations are responsible for escaping keys for their storage, but **must** be able to return the original, unmodified key (PSR-6's `CacheItemInterface::getKey()` does exactly that); - the characters `{}()/\@:` are **reserved** for future extensions and **must not** be supported. ## What happens with an illegal key Every method that takes a key declares that an illegal key **must** raise the standard's invalid-argument exception: | Standard | Methods | Exception interface | |---|---|---| | PSR-6 | `getItem()`, `getItems()`, `hasItem()`, `deleteItem()`, `deleteItems()` | `Psr\Cache\InvalidArgumentException` | | PSR-16 | `get()`, `set()`, `delete()`, `has()`, and the `*Multiple()` methods | `Psr\SimpleCache\InvalidArgumentException` | Both extend the standard's `CacheException` interface. This is the one exception a calling library should expect from normal use: PSR-6 asks implementations to trap storage errors rather than throw them, and PSR-16 reports failed writes and deletes as `false`, but an invalid key is the caller's bug, and it surfaces immediately. ## Keys that break in practice A library that builds keys from its inputs meets the reserved set quickly: 1. **URLs** contain `:` and `/`: `https://rates.example/v1` is illegal. 2. **E-mail addresses** contain `@`. 3. **Namespaced class names** contain `\`: `App\Rates\Provider` is illegal. 4. **Templates** such as `{pair}` use braces. 5. **Composite keys** such as `rate:EUR:USD` use colons, a separator many developers bring from key/value stores. Some implementations accept a few of these today, which is worse: the code works in development and throws when the application swaps in a stricter implementation. ## Building portable keys - Use a **short fixed prefix** per concern, with `.` or `_` as the separator: `rate.EURUSD`. - **Hash** any variable part that may contain other characters or grow long. `hash('xxh128', $url)` produces 32 hexadecimal characters, so `'rate.' . hash('xxh128', $url)` is 37 characters of legal ASCII. A 64-character `sha256` digest plus a prefix would exceed the guaranteed 64. - Keep the whole key within **64 characters**, even if your current backend allows more. - If you need to see the original input when debugging, log it next to the key; do not put it in the key. ## Why the reserved characters exist The specification only says the characters are reserved for future extensions. Reserving them means implementations cannot give them their own meanings, so a later standard could assign one without breaking conforming code. For a library author the consequence is simple: treat them as forbidden. ## Checklist for a framework-agnostic library - Validate or hash every externally influenced key part before it reaches the cache. - Write a unit test that feeds a URL and a namespaced class name through the key builder. - Do not catch `InvalidArgumentException` to hide it; fix the key builder instead. ## A note on the value side The key rules have a counterpart for values: both standards require implementations to return exactly what was stored, including its type, and to answer with a miss rather than corrupted data when that is impossible. Keys are strict so that values can be trusted.

  • Why is 'rate.' . hash('sha256', $url) a risky PSR-16 key?
    A `sha256` hex digest is 64 characters, so with the prefix the key is 69. PSR-16 only guarantees support up to 64 characters, so an implementation that stops at the minimum may reject it with `Psr\SimpleCache\InvalidArgumentException`. A shorter digest such as `xxh128` (32 hex characters) keeps it portable.
  • Should a library catch Psr\SimpleCache\InvalidArgumentException and skip caching?
    Not as a fix. That exception means the calling code built an illegal key, which is a bug in the library, not a storage outage. Catching it hides the bug and silently disables caching. Fix the key builder so every key is legal, and cover it with a test.

saying these in an interview costs you the question

  • Uses colons in keys like rate:EUR:USD because key/value stores allow them.
  • Assumes any string is a valid key if the current backend accepts it.
  • Believes keys may be any length because storage handles it.
  • Puts full URLs or class names straight into cache keys.
  • Catches InvalidArgumentException from the cache and silently skips caching.