Thursday 24 August 2023

How Can I Manually Revalidate a Dynamic Next.js Route, From an API Route?

I have a Next.js application with a dynamic page for displaying resources: /resource/[id]. Whenever a user edits (say) resource #5, I want to regenerate the page /resource/5 in Next's cache.

I have an API route (in my /pages directory) that handles the resource editing. From what I've read, I should be able to make it refresh the display route by doing:

response.revalidate(`/resource/${id}/`);

However, that doesn't work; I get the error:

Error: Failed to revalidate /resource/2153/: Invalid response 200
    at revalidate (/home/jeremy/GoblinCrafted/next/node_modules/next/dist/server/api-utils/node.js:388:15)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
- error unhandledRejection: Error: Failed to revalidate /resource/2153/: Invalid response 200
    at revalidate (/home/jeremy/GoblinCrafted/next/node_modules/next/dist/server/api-utils/node.js:388:15)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  digest: undefined

I guess Next is trying to revalidate through an HTTP request, but the revalidation endpoint moved? I'm not exactly sure why this fails.

EDIT: I dug into the Next source code, and it seems like revalidate is just making a mock request to the route that is being revalidated. If you look at node_modules/next/dist/server/router.js you'll see:

async revalidate({ urlPath , revalidateHeaders , opts  }) {
        const mocked = (0, _mockrequest.createRequestResponseMocks)({
            url: urlPath,
            headers: revalidateHeaders
        });
        const handler = this.getRequestHandler();
        await handler(new _node.NodeNextRequest(mocked.req), new _node.NodeNextResponse(mocked.res));
        await mocked.res.hasStreamed;
        if (mocked.res.getHeader("x-nextjs-cache") !== "REVALIDATED" && !(mocked.res.statusCode === 404 && opts.unstable_onlyGenerated)) {
            throw new Error(`Invalid response ${mocked.res.statusCode}`);
        }

My error is coming from that throw at the end ... but that doesn't make any sense to me, because when I log urlPath, it's just the path I'm trying to refresh (eg. /resource/5). When I try to hit that path with a GET request (either in my browser or through Postman) I don't get a redirect: I get a 200.

This led me to try adding a / to the end of my path:

response.revalidate(`/resource/${id}/`);

That got me a similar error, only this time it was for a 200 instead of a 308:

Error: Invalid response 200

So, it seems my status code doesn't actually matter: it's the mocked.res.getHeader("x-nextjs-cache") !== "REVALIDATED" part that's the problem. However, digging through the code, I only found one place that sets that header. It happens within a function within a function within a renderToResponseWithComponentsImpl function in node_modules/next/dist/esm/server/base-server.js:

if (isSSG && !this.minimalMode) {
        // set x-nextjs-cache header to match the header
        // we set for the image-optimizer
        res.setHeader("x-nextjs-cache", isOnDemandRevalidate ? "REVALIDATED" : cacheEntry.isMiss ? "MISS" : cacheEntry.isStale ? "STALE" : "HIT");
 }

... but when I add console.log statements it seems that code isn't being reached ... even though I see lots of other requests reaching it, and isSSG && !this.minimalMode is true for all of them.

So, in short, somehow the mock request revalidate makes isn't triggering the setting of that header, which then makes it fail ... but I have no clue why it's not getting to that header-setting code, because it's so deeply nested in the router code.

END EDIT

I also tried using revalidatePath, from next/cache:

revalidatePath(`/resource/[id]`);

but that also gives an error:

revalidatePath(`/resource/[id]`);

Error: Invariant: static generation store missing in revalidateTag /resource/[id]
    at revalidateTag (/home/me/project/next/node_modules/next/dist/server/web/spec-extension/revalidate-tag.js:15:15)
    at revalidatePath (/home/me/project/next/node_modules/next/dist/server/web/spec-extension/revalidate-path.js:13:45)
    at handler (webpack-internal:///(api)/./pages/api/resource.js:88:67)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

I think this is because revalidatePath is only intended to be used in the /app directory?

Finally, I found a reference to a response.unstable_revalidate method, which seemed to be designed for revalidating dynamic paths:

    response.unstable_revalidate(`/resource/${id}/`);

... but when I try to use it, it isn't there on the response:

TypeError: response.unstable_revalidate is not a function
    at handler (webpack-internal:///(api)/./pages/api/resource.js:88:18)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)


from How Can I Manually Revalidate a Dynamic Next.js Route, From an API Route?

No comments:

Post a Comment