cloudflare / cloudflare/vinext

unstable_cache: encode the synthetic cache item name so non-ASCII query params / callback names don't silently break caching

Open
#2,895 0 comments 0 reactions 0 assignees View on GitHub
nextjs-tracking
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.