Skip to content

APP_ROUTE entries are cached with empty tags, so revalidateTag/revalidatePath never invalidates route handlers #231

Description

@niklas-radyant

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

  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:

{
  "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:

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions