cloudflare / cloudflare/vinext
unstable_cache: encode the synthetic cache item name so non-ASCII query params / callback names don't silently break caching
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
Track Next.js fix for non-ASCII characters in the `unstable_cache` cache **item name** breaking caching.
Upstream commit: https://github.com/vercel/next.js/commit/4286a42e2ce968caf967da45dc415d722893c496 (#96937)
Fixes: https://github.com/vercel/next.js/issues/76286
Related refactor: https://github.com/vercel/next.js/commit/1f0cd93b83ebcb97b8de7542a6d34d300358cd9d (#96936) — renames `encodeCacheTag` → `encodeHeaderSafe`
Related (already closed) issue: #1138 encodes non-ASCII in cache **tags**. This is a distinct code path: the cache **item name** built by `unstable_cache`, not the tags or the cache key.
## Problem
`unstable_cache` assembles a synthetic cache item name (its `fetchUrl`) from two inputs:
```
`unstable_cache ${fetchUrlPrefix} ${cb.name ? ` ${cb.name}` : cacheKey}`
```
`fetchUrlPrefix` is built by `getFetchUrlPrefix`, which reads the pathname and search params out of the request URL. The pathname stays percent-encoded, but `URLSearchParams` returns **decoded** keys/values, so a non-ASCII query parameter lands unencoded in the name. The callback name (`cb.name`) can also hold a non-ASCII character.
A cache implementation may serialize this item name into an HTTP request header, whose values are limited to Latin-1. When the name holds a character above U+00FF, the conversion throws **before** the request is dispatched, so:
- the read never reaches the cache (nothing is found),
- the write that follows fails the same way (nothing is stored),
- the entry falls back to the origin on **every** render, with no reported error.
Reachable cases:
- Any dynamic route calling `unstable_cache` with a non-ASCII query parameter (whether or not the route reads `searchParams`, and including a param a caller appends).
- A cached callback whose name holds a non-ASCII character (usually only observable in dev; a production build renames the binding).
## Fix shape (upstream)
Encode the assembled name with `encodeHeaderSafe` (the renamed `encodeCacheTag`), after normalizing lone surrogates:
```ts
const fetchUrl = encodeHeaderSafe(
`unstable_cache ${fetchUrlPrefix} ${cb.name ? ` ${cb.name}` : cacheKey}`.toWellFormed()
)
```
- `encodeHeaderSafe` only replaces characters outside Node's valid header-value class, so the separating spaces and URL punctuation are preserved and the name keeps its documented shape.
- `toWellFormed()` replaces lone surrogates (which `cb.name` can hold and which `encodeURIComponent` rejects) with the replacement character — acceptable because the item name is only a debug label, not the cache key.
- Every name representable today is returned unchanged, so this is inert for existing entries.
The item name is a label: it is not the cache key (derived separately from the callback key parts and args), and the Suspense Cache API neither parses nor matches on it.
## Relevance to vinext
vinext reimplements `unstable_cache`. If our implementation constructs a comparable item name and any cache backend (e.g. a KV/data adapter, or Node-compatible post-processing) serializes it into a header, we hit the same silent cache-never-hits failure class. We should apply the same header-safe encoding (plus `toWellFormed`) to the item name at construction.
## Verification (upstream test)
`test/e2e/app-dir/non-ascii-cache-item-name/`:
- `/[slug]` requested with a non-ASCII segment + non-ASCII query param (anonymous callback)
- `/named-callback` requested under a pure-ASCII URL with a non-ASCII callback name (dev only)
- `/lone-surrogate` for the lone-surrogate case
- A deployment/cache-handler check asserts the entry is not recomputed on every request.
Contributor guide
Research direction
Start at vinext's unstable_cache implementation and trace construction of the synthetic cache item name (fetchUrl), including getFetchUrlPrefix and callback names. Compare the upstream scenarios in test/e2e/app-dir/non-ascii-cache-item-name/; done means non-ASCII query parameters, callback names, and lone surrogates no longer prevent cache reuse, including the deployment/cache-handler check.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100