Track Next.js fix for non-ASCII characters in the unstable_cache cache item name breaking caching.
Upstream commit: vercel/next.js@4286a42 (#96937)
Fixes: vercel/next.js#76286
Related refactor: vercel/next.js@1f0cd93 (#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:
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.
Track Next.js fix for non-ASCII characters in the
unstable_cachecache item name breaking caching.Upstream commit: vercel/next.js@4286a42 (#96937)
Fixes: vercel/next.js#76286
Related refactor: vercel/next.js@1f0cd93 (#96936) — renames
encodeCacheTag→encodeHeaderSafeRelated (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_cacheassembles a synthetic cache item name (itsfetchUrl) from two inputs:fetchUrlPrefixis built bygetFetchUrlPrefix, which reads the pathname and search params out of the request URL. The pathname stays percent-encoded, butURLSearchParamsreturns 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:
Reachable cases:
unstable_cachewith a non-ASCII query parameter (whether or not the route readssearchParams, and including a param a caller appends).Fix shape (upstream)
Encode the assembled name with
encodeHeaderSafe(the renamedencodeCacheTag), after normalizing lone surrogates:encodeHeaderSafeonly 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 (whichcb.namecan hold and whichencodeURIComponentrejects) with the replacement character — acceptable because the item name is only a debug label, not the cache key.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 (plustoWellFormed) 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-callbackrequested under a pure-ASCII URL with a non-ASCII callback name (dev only)/lone-surrogatefor the lone-surrogate case