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
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
- A route handler with
export const revalidate = 3600 — e.g. app/sitemap.xml/route.ts.
- Request it once so it caches.
revalidatePath('/sitemap.xml').
- Request again.
Stored entry, before and after:
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:
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
APP_ROUTEentries are stored withtags: [], sorevalidateTag— and thereforerevalidatePath— 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 forPAGES.Cause
In
set(), tags are taken fromctx.tagsand 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
APP_ROUTEfalls todefault, keepingctx.tags— and Next does not populatectx.tagsfor route handlers. It does send the tags, in thex-next-cache-tagsresponse header thatgetTagsFromHeaders()already reads forAPP_PAGE.Measured on Next 15.5.22, logging the arguments
set()receives:So the implicit path tag
_N_T_/sitemaps/products.xmlis available and simply not persisted. SincerevalidatePath('/x')works by revalidating_N_T_/x, nothing can invalidate the entry.Reproduction
export const revalidate = 3600— e.g.app/sitemap.xml/route.ts.revalidatePath('/sitemap.xml').Stored entry, before and after:
{ "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" } } }lastModifiednever advances. The same test against anAPP_PAGEpasses, 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 skipsAPP_ROUTE, these entries get the defaultstaleAge(~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
revalidatePathcalls silently did nothing.robots.txtand OG image routes have the same shape.Suggested fix
APP_ROUTEcarries the same headerAPP_PAGEdoes, so the existing helper covers it:Happy to open a PR for this and for #224 if that helps.
Versions
@fortedigital/nextjs-cache-handler2.5.3, and confirmed still present in 3.2.1 and onmainnext15.5.22, App Router,output: standalone, self-hostedredis5.12.1 with theredis-stringshandler