fortedigital / fortedigital/nextjs-cache-handler
APP_ROUTE entries are cached with empty tags, so revalidateTag/revalidatePath never invalidates route handlers
- Dominant language
- TypeScript
- Stars
- 187
- Forks
- 29
- PR merge metrics
- No merged PRs in 30d
Description
`APP_ROUTE` entries are stored with `tags: []`, so `revalidateTag` — and therefore `revalidatePath` — never matches them. Route handlers stay cached until they expire on their own.
Sibling of #224 (same `kind`, different helper), and the same class of bug that #227 fixed for `PAGES`.
## Cause
In `set()`, tags are taken from `ctx.tags` and only re-derived for two kinds:
https://github.com/fortedigital/nextjs-cache-handler/blob/main/packages/nextjs-cache-handler/src/handlers/cache-handler.ts#L788-L803
```ts
switch (value?.kind) {
case "APP_PAGE": {
cacheHandlerValueTags = getTagsFromHeaders(value.headers ?? {});
break;
}
case "PAGES": {
const pathTag = getImplicitPathTag(cacheKey);
...
}
default: { // ← APP_ROUTE lands here
break;
}
}
```
`APP_ROUTE` falls to `default`, keeping `ctx.tags` — and Next does not populate `ctx.tags` for route handlers. It does send the tags, in the `x-next-cache-tags` response header that `getTagsFromHeaders()` already reads for `APP_PAGE`.
Measured on Next 15.5.22, logging the arguments `set()` receives:
```
APP_ROUTE /sitemaps/products.xml
ctx keys ["cacheControl", "isRoutePPREnabled", "isFallback"]
ctx.tags (absent)
ctx.cacheControl { revalidate: 3600 }
headers['x-next-cache-tags'] _N_T_/layout,
_N_T_/sitemaps/layout,
_N_T_/sitemaps/products.xml/layout,
_N_T_/sitemaps/products.xml/route,
_N_T_/sitemaps/products.xml
```
So the implicit path tag `_N_T_/sitemaps/products.xml` is available and simply not persisted. Since `revalidatePath('/x')` works by revalidating `_N_T_/x`, nothing can invalidate the entry.
## Reproduction
1. A route handler with `export const revalidate = 3600` — e.g. `app/sitemap.xml/route.ts`.
2. Request it once so it caches.
3. `revalidatePath('/sitemap.xml')`.
4. Request again.
Stored entry, before and after:
```jsonc
{
"lastModified": 1785665885071, // unchanged after revalidatePath
"lifespan": { "staleAge": 31536000, ... },
"tags": [], // ← empty
"value": {
"kind": "APP_ROUTE",
"headers": { "x-next-cache-tags": "_N_T_/layout,…,_N_T_/sitemap.xml" }
}
}
```
`lastModified` never advances. The same test against an `APP_PAGE` passes, which is what makes this easy to miss — a page-only test of revalidation looks completely healthy while every route handler is frozen.
## Impact
Worse in combination with #224. Because `resolveRevalidateValue()` also skips `APP_ROUTE`, these entries get the default `staleAge` (~1 year) *and* cannot be revalidated on demand — so a route handler is effectively cached forever.
For us that was the XML sitemaps: they would have sat frozen in Redis while the content pipeline's `revalidatePath` calls silently did nothing. `robots.txt` and OG image routes have the same shape.
## Suggested fix
`APP_ROUTE` carries the same header `APP_PAGE` does, so the existing helper covers it:
```ts
case "APP_PAGE":
case "APP_ROUTE": {
cacheHandlerValueTags = getTagsFromHeaders(value.headers ?? {});
break;
}
```
Happy to open a PR for this and for #224 if that helps.
## Versions
- `@fortedigital/nextjs-cache-handler` 2.5.3, and confirmed still present in 3.2.1 and on `main`
- `next` 15.5.22, App Router, `output: standalone`, self-hosted
- `redis` 5.12.1 with the `redis-strings` handler
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in packages/nextjs-cache-handler/src/handlers/cache-handler.ts around lines 788-803 and trace set() alongside getTagsFromHeaders(). Reproduce with an APP_ROUTE such as app/sitemap.xml/route.ts, then verify that revalidatePath('/sitemap.xml') invalidates the cached entry and its persisted tags are no longer empty.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, redis, typescript
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100