Open joostmeijles opened 2 months ago
This behavior is changing with Next.js 15 (where GET
Route Handlers are not cached by default).
Would you might updating to the latest https://nextjs.org/blog/next-15-rc and retesting?
Hey @leerob, thanks for you answer.
What if I want to cache GET request?
In my case I have a hCMS with next14.2.3.
The flow is I generate dynamic slug pages at build time, all requests are marked with a tag "CMS-content". And then call revalidateTag to make sure all the pages have a up to date content.
In 99% of cases it works as expected, but in 1% updated pages return X-Vercel-Cache: STALE and never changes. In order to see updates I have to rebuild the project or purge cache.
Maybe you could point me to where the problem could be, since generały it works correct.
❤️🫂
This behavior is changing with Next.js 15 (where
GET
Route Handlers are not cached by default).Would you might updating to the latest https://nextjs.org/blog/next-15-rc and retesting?
Upgraded, but seeing the exact same behavior: https://github.com/joostmeijles/route-handler-stale/pull/1
For clarity, STALE
still means you can serve static content.
The response was served from the edge cache. A background request to the origin server was made to update the content.
Source: https://vercel.com/docs/edge-network/headers#x-vercel-cache
Are you wanting to disable the ability to use ISR entirely here? (which is where the background revalidation functionality is coming from).
Are you wanting to disable the ability to use ISR entirely here? (which is where the background revalidation functionality is coming from).
I want to get the same behavior as for a page based route segment. So only perform the background request when used data changes / is revalidated (for simplicity I left this out of the example).
An example with a page based route segment can be found here: visiting https://routehandler-stale.vercel.app/items-page/a returns a HIT iso STALE (code: https://github.com/joostmeijles/route-handler-stale/blob/main/app/items-page/%5Bslug%5D/page.tsx).
So I would expect that https://github.com/joostmeijles/route-handler-stale/blob/main/app/items/%5Bslug%5D/route.ts results always in HIT (and not STALE) for subsequent requests (when the used data did not change).
Can you make the deployment link public so I can view it myself? It's currently private.
https://routehandler-stale.vercel.app/items-page/a is public.
Which link do you mean?
Thank you. I'm seeing HIT
here.
Exactly. That’s the expected behavior as there is no stale data to revalidate.
For https://routehandler-stale.vercel.app/items/a I would expect the same as there is nothing to revalidate as well.
It's using ISR for the dynamic route segments. generateStaticParams
is not returning any options for the items, so no pages are generated during the build to be statically generated. Instead, the pages are being generated at runtime and then cached. This is why you are seeing STALE
first.
generateStaticParams
dynamicParams = false
(docs)I want the route response to be generated at runtime and then cached. Other routes should result in a 404. In code that would be something like:
export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
const slug = params.slug;
// Fetch data for slug
const res= await fetch(`http://example/{slug}`, next: { tags: `mytag-${slug}`});
// Returns OK for 'a', 'b', 'c' slugs
if (res.ok) {
return NextResponse.json({ slug });
}
// Other routes do not have data and should return a 404
return NextResponse.json({ message: "Not found"}, { status: 404 });
}
export async function generateStaticParams() {
return [];
}
When visiting https://routehandler-stale.vercel.app/items/a I expect as X-Vercel-Cache
header value:
Got it. As mentioned above, you're then looking for:
export const dynamicParams = false;
https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
That works locally, but results in a 404 on Vercel: https://routehandler-stale.vercel.app/items-dynamicparams/a
(code: https://github.com/joostmeijles/route-handler-stale/commit/178aed211af2e50f9cf4348b1946b396c952ed7a)
That is correct, because they weren't in generateStaticParams
.
export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
let slug = params.slug;
let res = await fetch(`http://example/{slug}`, next: { tags: `mytag-${slug}`});
return NextResponse.json({ slug });
}
export async function generateStaticParams() {
return ['a', 'b', 'c'];
}
The possible slug
params aren't known at build time, so I can't do that.
Please note that specifying slug values when using page.tsx
isn't necessary: https://github.com/joostmeijles/route-handler-stale/blob/main/app/items-page/%5Bslug%5D/page.tsx
If the values are not possible to be known at build time, then there is no way to statically prerender those files. The behavior you are describing for the page should be the same for the route handler. A route handler is basically the lowest level abstraction of a page, which is why they have the same route segment configuration options.
The behavior you are describing where the first visit generates the page, and then the result is cached on the Vercel CDN for subsequent requests, is how both pages and route handlers work. If you are wanting to extend the revalidation period, so you are able to serve cached files longer, you can use the revalidate
option.
export const revalidate = 3600;
The issue is that the behaviour of route.ts
and page.tsx
is not the same. route.ts
always returns STALE while it should return HIT
on subsequent requests. This causes unnecessary function invocations on the origin server as the result can be cached (in the edge cache).
page.tsx
)The following page.tsx
code
// page.tsx file
export default async function Page({ params }: { params: { slug: string } }) {
return <span>{params.slug}</span>
}
export async function generateStaticParams() {
return [];
}
returns upon visiting a slug
, e.g. https://routehandler-stale.vercel.app/items-page/a
X-Vercel-Cache: MISS
X-Vercel-Cache: HIT
route.ts
)The following route.ts
code
// route.ts file
export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
const slug = params.slug
return NextResponse.json({ slug })
}
export async function generateStaticParams() {
return [];
}
returns upon visiting a slug
, e.g. https://routehandler-stale.vercel.app/items/a
X-Vercel-Cache: STALE
X-Vercel-Cache: STALE
Possible duplicate of #62195
Possible duplicate of #62195
Looks largely the same, but this issue is route.ts
specific while #62195 is about the page.tsx
behavior.
Note that the provided solution (by https://github.com/vercel/next.js/issues/62195#issuecomment-1952091312) to set dynamicParams = true
does not solve it. See https://github.com/joostmeijles/route-handler-stale/pull/2
@leerob I strongly believe this is not solved and is a bug. Can you please have a look at my clarification in https://github.com/vercel/next.js/issues/65814#issuecomment-2154209928?
Running the examples in minimalMode
(Vercel platform mode) results in the same x-next-cache-tags
for the route and page examples.
Running https://github.com/joostmeijles/route-handler-stale/blob/main/app/items/%5Bslug%5D/route.ts gives:
> node .\scripts\minimal-server.js C:\Repos\route-handler-stale items/a
'x-next-cache-tags' => {
name: 'x-next-cache-tags',
value: '_N_T_/layout,_N_T_/items/layout,_N_T_/items/[slug]/layout,_N_T_/items/[slug]/route,_N_T_/items/a'
}
Running https://github.com/joostmeijles/route-handler-stale/blob/main/app/items-page/%5Bslug%5D/page.tsx gives:
> node .\scripts\minimal-server.js C:\Repos\route-handler-stale items-page/a
'x-next-cache-tags' => {
name: 'x-next-cache-tags',
value: '_N_T_/layout,_N_T_/items-page/layout,_N_T_/items-page/[slug]/layout,_N_T_/items-page/[slug]/page,_N_T_/items-page/a'
}
In addition, when running the examples locally the returned X-Nextjs-Cache
header always correctly contains HIT for subsequent visits.
@joostmeijles Taking a look!
@joostmeijles I'm not sure if generateStaticParams
should be used alongside Route Handlers generateStaticParams
instead?
When I use export const dynamic = 'force-static'
on the latest canary. I got a MISS
on first request, then got subsequent HIT
.
@samcx tried with export const dynamic = 'force-static'
(only), but it behaves the same for me. I get a MISS
on first request and STALE
on subsequent requests.
See https://github.com/joostmeijles/route-handler-stale/blob/main/app/items-force-static/%5Bslug%5D/route.ts and https://routehandler-stale.vercel.app/items-force-static/a
First request
Second request
Please note that I see also STALE
on your test URL: https://65814-route-handler-stale.vercel.app/items/a
@joostmeijles Hmm seeing a few STALE
, but mostly HIT
. Could be infra-related. Will see to it internally!
Also, are you always seeing STALE
still for my Deployment?
@joostmeijles Hmm seeing a few
STALE
, but mostlyHIT
. Are you always seeingSTALE
? Could be infra-related. Will see to it internally!
Always seeing STALE
here for my example. For your example 50/50 HIT/STALE. Happy to jump on a call to demo it.
Correction: HIT
now also a number of times for https://routehandler-stale.vercel.app/items-force-static/a . STALE
still present though.
@joostmeijles Thanks for confirming. Will be looking to see why we're getting STALE
here!
@samcx do you have an update on the progress?
Link to the code that reproduces this issue
https://github.com/joostmeijles/route-handler-stale/tree/main
Route handler example: https://github.com/joostmeijles/route-handler-stale/blob/main/app/items/%5Bslug%5D/route.ts
To Reproduce
x-nextjs-cache: MISS
x-nextjs-cache: HIT
X-Vercel-Cache: STALE
Current vs. Expected behavior
Currently always
X-Vercel-Cache: STALE
is returned.Expected is
X-Vercel-Cache: MISS
on first visit andX-Vercel-Cache: HIT
for subsequent visits.Provide environment information
Which area(s) are affected? (Select all that apply)
App Router
Which stage(s) are affected? (Select all that apply)
Vercel (Deployed)
Additional context
No response