Skip to content

Commit 6fe9b55

Browse files
fix: make sure that fetch cache sets are properly awaited
Next.js does not await promises that update the incremental cache for fetch requests, that is needed in our runtime otherwise the cache updates get lost, so this change makes sure that the promise is properly awaited via `waitUntil`
1 parent c3dc401 commit 6fe9b55

File tree

4 files changed

+84
-0
lines changed

4 files changed

+84
-0
lines changed

.changeset/few-ducks-listen.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
fix: make sure that fetch cache `set`s are properly awaited
6+
7+
Next.js does not await promises that update the incremental cache for fetch requests,
8+
that is needed in our runtime otherwise the cache updates get lost, so this change
9+
makes sure that the promise is properly awaited via `waitUntil`

packages/cloudflare/src/cli/build/bundle-server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { patchVercelOgLibrary } from "./patches/ast/patch-vercel-og-library.js";
1010
import { patchWebpackRuntime } from "./patches/ast/webpack-runtime.js";
1111
import * as patches from "./patches/index.js";
1212
import { ContentUpdater } from "./patches/plugins/content-updater.js";
13+
import { patchFetchCacheSetMissingWaitUntil } from "./patches/plugins/fetch-cache-wait-until.js";
1314
import { patchLoadInstrumentation } from "./patches/plugins/load-instrumentation.js";
1415
import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
1516
import { fixRequire } from "./patches/plugins/require.js";
@@ -87,6 +88,7 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
8788
fixRequire(updater),
8889
handleOptionalDependencies(optionalDependencies),
8990
patchLoadInstrumentation(updater),
91+
patchFetchCacheSetMissingWaitUntil(updater),
9092
// Apply updater updaters, must be the last plugin
9193
updater.plugin,
9294
],
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, test } from "vitest";
2+
3+
import { patchCode } from "../ast/util.js";
4+
import { rule } from "./fetch-cache-wait-until.js";
5+
6+
describe("patchFetchCacheSetMissingWaitUntil", () => {
7+
test("patch", () => {
8+
const code = `
9+
{
10+
let [o4, a2] = (0, d2.cloneResponse)(e3);
11+
return o4.arrayBuffer().then(async (e4) => {
12+
var a3;
13+
let i4 = Buffer.from(e4), s3 = { headers: Object.fromEntries(o4.headers.entries()), body: i4.toString("base64"), status: o4.status, url: o4.url };
14+
null == $ || null == (a3 = $.serverComponentsHmrCache) || a3.set(n2, s3), F && await H.set(n2, { kind: c2.CachedRouteKind.FETCH, data: s3, revalidate: t5 }, { fetchCache: true, revalidate: r4, fetchUrl: _, fetchIdx: q, tags: A2 });
15+
}).catch((e4) => console.warn("Failed to set fetch cache", u4, e4)).finally(X), a2;
16+
}`;
17+
18+
expect(patchCode(code, rule)).toMatchInlineSnapshot(`
19+
"{
20+
let [o4, a2] = (0, d2.cloneResponse)(e3);
21+
globalThis.__openNextAls.getStore().waitUntil(o4.arrayBuffer().then(async (e4) => {
22+
var a3;
23+
let i4 = Buffer.from(e4), s3 = { headers: Object.fromEntries(o4.headers.entries()), body: i4.toString("base64"), status: o4.status, url: o4.url };
24+
null == $ || null == (a3 = $.serverComponentsHmrCache) || a3.set(n2, s3), F && await H.set(n2, { kind: c2.CachedRouteKind.FETCH, data: s3, revalidate: t5 }, { fetchCache: true, revalidate: r4, fetchUrl: _, fetchIdx: q, tags: A2 });
25+
}).catch((e4) => console.warn("Failed to set fetch cache", u4, e4)).finally(X));
26+
return a2;
27+
28+
}"
29+
`);
30+
});
31+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { patchCode } from "../ast/util.js";
2+
import type { ContentUpdater } from "./content-updater.js";
3+
4+
/**
5+
* The following Next.js code sets values in the incremental cache for fetch calls:
6+
* https://github.com/vercel/next.js/blob/e5fc495e3d4/packages/next/src/server/lib/patch-fetch.ts#L690-L728
7+
*
8+
* The issue here is that this promise is never awaited in the Next.js code (since in a standard node.js server
9+
* the promise will eventually simply just run) but we do need to run it inside `waitUntil` (so that the worker
10+
* is not killed before the promise is fully executed), without that this promise gets discarded and values
11+
* don't get saved in the incremental cache.
12+
*
13+
* This function wraps the promise in a `waitUntil` call (retrieved from `globalThis.__openNextAls.getStore()`).
14+
*/
15+
export function patchFetchCacheSetMissingWaitUntil(updater: ContentUpdater) {
16+
return updater.updateContent(
17+
"patch-fetch-cache-set-missing-wait-until",
18+
{ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ },
19+
({ contents }) => {
20+
if (/Failed to set fetch cache/.test(contents)) {
21+
return patchCode(contents, rule);
22+
}
23+
}
24+
);
25+
}
26+
27+
export const rule = `
28+
rule:
29+
kind: return_statement
30+
all:
31+
- pattern: return $PROMISE, $CLONED2
32+
- regex: Failed to set fetch cache
33+
- follows:
34+
kind: lexical_declaration
35+
all:
36+
- pattern: let [$CLONED1, $CLONED2]
37+
38+
39+
fix: |
40+
globalThis.__openNextAls.getStore().waitUntil($PROMISE);
41+
return $CLONED2;
42+
`;

0 commit comments

Comments
 (0)