fortedigital / fortedigital/nextjs-cache-handler

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
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.