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

Open Beginner friendly
#231 1 comment 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
76/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
next.js, redis, typescript
Domain
backend, databases

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.

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

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
Dominant language
TypeScript
Stars
189
Forks
29
PR merge metrics
No merged PRs in 30d

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from fortedigital/nextjs-cache-handler

All issues in fortedigital/nextjs-cache-handler

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.