vercel / next.js

The React Framework
https://nextjs.org
MIT License
123.53k stars 26.35k forks source link

Route handler always returns `X-Vercel-Cache: STALE` #65814

Open joostmeijles opened 2 months ago

joostmeijles commented 2 months ago

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

  1. Start the application in production mode
  2. Visit http://localhost:3000/items/a
  3. Inspect the response headers and verify that first visit has x-nextjs-cache: MISS
  4. Visit http://localhost:3000/items/a
  5. Inspect the response headers and verify that first visit has x-nextjs-cache: HIT
  6. Deploy to Vercel
  7. Visit https://routehandler-stale.vercel.app/items/a
  8. Inspect the response headers and verify that it has always 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 and X-Vercel-Cache: HIT for subsequent visits.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.2.0: Fri Nov 11 02:04:44 PST 2022; root:xnu-8792.61.2~4/RELEASE_ARM64_T8103
  Available memory (MB): 8192
  Available CPU cores: 8
Binaries:
  Node: 20.12.2
  npm: 10.5.0
  Yarn: N/A
  pnpm: 8.15.7
Relevant Packages:
  next: 14.3.0-canary.63 // Latest available version is detected (14.3.0-canary.63).
  eslint-config-next: N/A
  react: 19.0.0-beta-4508873393-20240430
  react-dom: 19.0.0-beta-4508873393-20240430
  typescript: 5.1.3
Next.js Config:
  output: N/A

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

leerob commented 1 month 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?

dogfrogfog commented 1 month ago

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.

❤️🫂

joostmeijles commented 1 month 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?

Upgraded, but seeing the exact same behavior: https://github.com/joostmeijles/route-handler-stale/pull/1

leerob commented 1 month ago

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

image

Are you wanting to disable the ability to use ISR entirely here? (which is where the background revalidation functionality is coming from).

joostmeijles commented 1 month ago

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

leerob commented 1 month ago

Can you make the deployment link public so I can view it myself? It's currently private.

joostmeijles commented 1 month ago

https://routehandler-stale.vercel.app/items-page/a is public.

Which link do you mean?

leerob commented 1 month ago

Thank you. I'm seeing HIT here.

CleanShot 2024-06-04 at 10 39 16@2x

joostmeijles commented 1 month ago

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.

leerob commented 1 month ago

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.

joostmeijles commented 1 month ago

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:

leerob commented 1 month ago

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

joostmeijles commented 1 month ago

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)

leerob commented 1 month ago

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'];
}
joostmeijles commented 1 month ago

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

leerob commented 1 month ago

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;
joostmeijles commented 1 month ago

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

Pages handler (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

Route handler (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

rijk commented 1 month ago

Possible duplicate of #62195

joostmeijles commented 1 month ago

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

joostmeijles commented 2 weeks ago

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

joostmeijles commented 2 weeks ago

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.

samcx commented 1 week ago

@joostmeijles Taking a look!

samcx commented 1 week ago

@joostmeijles I'm not sure if generateStaticParams should be used alongside Route Handlers :hmmm:. Is there a reason why you are using 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.

CleanShot 2024-07-16 at 15 04 45@2x

joostmeijles commented 1 week ago

@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

image

Second request

image

Please note that I see also STALE on your test URL: https://65814-route-handler-stale.vercel.app/items/a

image
samcx commented 1 week ago

@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 commented 1 week ago

@joostmeijles Hmm seeing a few STALE, but mostly HIT. Are you always seeing STALE? 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.

joostmeijles commented 1 week ago

Correction: HIT now also a number of times for https://routehandler-stale.vercel.app/items-force-static/a . STALE still present though.

samcx commented 1 week ago

@joostmeijles Thanks for confirming. Will be looking to see why we're getting STALE here!

joostmeijles commented 6 days ago

@samcx do you have an update on the progress?