Skip to content

Commit 2512093

Browse files
authored
Fix falback issue (#252)
* fix fallback false * fix plugin not being applied * fix get could fail on batchWriteError * fix don't try to fetch missing key * updated docs
1 parent 6f5bd12 commit 2512093

File tree

5 files changed

+86
-53
lines changed

5 files changed

+86
-53
lines changed

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -414,16 +414,19 @@ This cost estimate is based on the `us-east-1` region pricing and does not consi
414414

415415
#### WORKAROUND: Patch fetch behaviour for ISR. Only for [email protected]+
416416

417-
In order to make fetch work as expected with ISR, you need to patch the `fetch` function in your app. Just add this lines to your root layout component:
417+
If you use ISR and fetch in your app, you may encounter a bug that makes your revalidate values inconsistent.
418+
The issue is that it revalidates using the lowest revalidate of all fetch calls in your page, regardless of their individual values. To fix this bug, you need to modify the fetch function in your root layout component with the following code snippet
418419

419420
```ts
420-
const asyncStorage = require("next/dist/client/components/static-generation-async-storage.external");
421-
//@ts-ignore
422-
const staticStore = (fetch as any).__nextGetStaticStore?.() || asyncStorage.staticGenerationAsyncStorage;
423-
const store = staticStore.getStore();
424-
store.isOnDemandRevalidate = store.isOnDemandRevalidate && !(process.env.OPEN_NEXT_ISR === 'true');
421+
export default function RootLayout() {
422+
const asyncStorage = require("next/dist/client/components/static-generation-async-storage.external");
423+
//@ts-ignore
424+
const staticStore = (fetch as any).__nextGetStaticStore?.() || asyncStorage.staticGenerationAsyncStorage;
425+
const store = staticStore.getStore();
426+
store.isOnDemandRevalidate = store.isOnDemandRevalidate && !(process.env.OPEN_NEXT_ISR === 'true');
427+
return <>...</>;
428+
}
425429
```
426-
Without this workaround, if you have 2 fetch calls in your page with different revalidate values, both will use the smallest value during ISR revalidation.
427430

428431
#### WORKAROUND: Create one cache behavior per top-level file and folder in `public/` (AWS specific)
429432

packages/open-next/src/adapters/cache.ts

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,18 @@ export default class S3Cache {
131131
public async get(key: string, options?: boolean | { fetchCache?: boolean }) {
132132
const isFetchCache =
133133
typeof options === "object" ? options.fetchCache : options;
134+
const keys = await this.listS3Object(key);
135+
if (keys.length === 0) return null;
136+
debug("keys", keys);
134137
return isFetchCache
135-
? this.getFetchCache(key)
136-
: this.getIncrementalCache(key);
138+
? this.getFetchCache(key, keys)
139+
: this.getIncrementalCache(key, keys);
137140
}
138141

139-
async getFetchCache(key: string) {
142+
async getFetchCache(key: string, keys: string[]) {
140143
debug("get fetch cache", { key });
141144
try {
142-
const { Body, LastModified } = await this.getS3Object(key, "fetch");
145+
const { Body, LastModified } = await this.getS3Object(key, "fetch", keys);
143146
const lastModified = await this.getHasRevalidatedTags(
144147
key,
145148
LastModified?.getTime(),
@@ -149,6 +152,8 @@ export default class S3Cache {
149152
return null;
150153
}
151154

155+
if (Body === null) return null;
156+
152157
return {
153158
lastModified,
154159
value: JSON.parse((await Body?.transformToString()) ?? "{}"),
@@ -159,17 +164,16 @@ export default class S3Cache {
159164
}
160165
}
161166

162-
async getIncrementalCache(key: string): Promise<CacheHandlerValue | null> {
163-
const keys = await this.listS3Object(key);
164-
if (keys.length === 0) return null;
165-
debug("keys", keys);
166-
167+
async getIncrementalCache(
168+
key: string,
169+
keys: string[],
170+
): Promise<CacheHandlerValue | null> {
167171
if (keys.includes(this.buildS3Key(key, "body"))) {
168172
debug("get body cache ", { key });
169173
try {
170174
const [{ Body, LastModified }, { Body: MetaBody }] = await Promise.all([
171-
this.getS3Object(key, "body"),
172-
this.getS3Object(key, "meta"),
175+
this.getS3Object(key, "body", keys),
176+
this.getS3Object(key, "meta", keys),
173177
]);
174178
const body = await Body?.transformToByteArray();
175179
const meta = JSON.parse((await MetaBody?.transformToString()) ?? "{}");
@@ -198,9 +202,9 @@ export default class S3Cache {
198202
try {
199203
const [{ Body, LastModified }, { Body: PageBody }, { Body: MetaBody }] =
200204
await Promise.all([
201-
this.getS3Object(key, "html"),
202-
this.getS3Object(key, isJson ? "json" : "rsc"),
203-
this.getS3Object(key, "meta"),
205+
this.getS3Object(key, "html", keys),
206+
this.getS3Object(key, isJson ? "json" : "rsc", keys),
207+
this.getS3Object(key, "meta", keys),
204208
]);
205209
const lastModified = await this.getHasRevalidatedTags(
206210
key,
@@ -234,7 +238,11 @@ export default class S3Cache {
234238
if (keys.includes(this.buildS3Key(key, "redirect"))) {
235239
debug("get redirect cache", { key });
236240
try {
237-
const { Body, LastModified } = await this.getS3Object(key, "redirect");
241+
const { Body, LastModified } = await this.getS3Object(
242+
key,
243+
"redirect",
244+
keys,
245+
);
238246
return {
239247
lastModified: LastModified?.getTime(),
240248
value: JSON.parse((await Body?.transformToString()) ?? "{}"),
@@ -404,23 +412,27 @@ export default class S3Cache {
404412
}
405413

406414
private async batchWriteDynamoItem(req: { path: string; tag: string }[]) {
407-
await Promise.all(
408-
this.chunkArray(req, 25).map((Items) => {
409-
return this.dynamoClient.send(
410-
new BatchWriteItemCommand({
411-
RequestItems: {
412-
[CACHE_DYNAMO_TABLE ?? ""]: Items.map((Item) => ({
413-
PutRequest: {
414-
Item: {
415-
...this.buildDynamoObject(Item.path, Item.tag),
415+
try {
416+
await Promise.all(
417+
this.chunkArray(req, 25).map((Items) => {
418+
return this.dynamoClient.send(
419+
new BatchWriteItemCommand({
420+
RequestItems: {
421+
[CACHE_DYNAMO_TABLE ?? ""]: Items.map((Item) => ({
422+
PutRequest: {
423+
Item: {
424+
...this.buildDynamoObject(Item.path, Item.tag),
425+
},
416426
},
417-
},
418-
})),
419-
},
420-
}),
421-
);
422-
}),
423-
);
427+
})),
428+
},
429+
}),
430+
);
431+
}),
432+
);
433+
} catch (e) {
434+
error("Failed to batch write dynamo item", e);
435+
}
424436
}
425437

426438
private buildDynamoKey(key: string) {
@@ -469,16 +481,24 @@ export default class S3Cache {
469481
Prefix: `${this.buildS3KeyPrefix(key)}.`,
470482
}),
471483
);
472-
return (Contents ?? []).map(({ Key }) => Key);
484+
return (Contents ?? []).map(({ Key }) => Key) as string[];
473485
}
474486

475-
private getS3Object(key: string, extension: Extension) {
476-
return this.client.send(
477-
new GetObjectCommand({
478-
Bucket: CACHE_BUCKET_NAME,
479-
Key: this.buildS3Key(key, extension),
480-
}),
481-
);
487+
private async getS3Object(key: string, extension: Extension, keys: string[]) {
488+
try {
489+
if (!keys.includes(this.buildS3Key(key, extension)))
490+
return { Body: null, LastModified: null };
491+
const result = await this.client.send(
492+
new GetObjectCommand({
493+
Bucket: CACHE_BUCKET_NAME,
494+
Key: this.buildS3Key(key, extension),
495+
}),
496+
);
497+
return result;
498+
} catch (e) {
499+
error("This error can usually be ignored : ", e);
500+
return { Body: null, LastModified: null };
501+
}
482502
}
483503

484504
private putS3Object(

packages/open-next/src/build.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -518,14 +518,17 @@ async function createServerBundle(monorepoRoot: string) {
518518
compareSemver(options.nextVersion, "13.4.13") >= 0
519519
? [
520520
openNextPlugin({
521+
name: "opennext-13.4.13-serverHandler",
521522
target: /plugins\/serverHandler\.js/g,
522523
replacements: ["./serverHandler.replacement.js"],
523524
}),
524525
openNextPlugin({
526+
name: "opennext-13.4.13-util",
525527
target: /plugins\/util\.js/g,
526528
replacements: ["./util.replacement.js"],
527529
}),
528530
openNextPlugin({
531+
name: "opennext-13.4.13-default",
529532
target: /plugins\/routing\/default\.js/g,
530533
replacements: ["./default.replacement.js"],
531534
}),
@@ -535,18 +538,17 @@ async function createServerBundle(monorepoRoot: string) {
535538
if (compareSemver(options.nextVersion, "13.5.1") >= 0) {
536539
plugins = [
537540
openNextPlugin({
541+
name: "opennext-13.5-serverHandler",
538542
target: /plugins\/serverHandler\.js/g,
539543
replacements: ["./13.5/serverHandler.js"],
540544
}),
541545
openNextPlugin({
546+
name: "opennext-13.5-util",
542547
target: /plugins\/util\.js/g,
543-
replacements: ["./13.5/util.js"],
544-
}),
545-
openNextPlugin({
546-
target: /plugins\/util\.js/g,
547-
replacements: ["./util.replacement.js"],
548+
replacements: ["./13.5/util.js", "./util.replacement.js"],
548549
}),
549550
openNextPlugin({
551+
name: "opennext-13.5-default",
550552
target: /plugins\/routing\/default\.js/g,
551553
replacements: ["./default.replacement.js"],
552554
}),

packages/open-next/src/plugin.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Plugin } from "esbuild";
66
export interface IPluginSettings {
77
target: RegExp;
88
replacements: string[];
9+
name?: string;
910
}
1011

1112
const overridePattern = /\/\/#override (\w+)\n([\s\S]*?)\n\/\/#endOverride/gm;
@@ -46,9 +47,10 @@ const importPattern = /\/\/#import([\s\S]*?)\n\/\/#endImport/gm;
4647
export default function openNextPlugin({
4748
target,
4849
replacements,
50+
name,
4951
}: IPluginSettings): Plugin {
5052
return {
51-
name: "opennext",
53+
name: name ?? "opennext",
5254
setup(build) {
5355
build.onLoad({ filter: target }, async (args) => {
5456
let contents = await readFile(args.path, "utf-8");
@@ -70,6 +72,9 @@ export default function openNextPlugin({
7072
const pattern = new RegExp(
7173
`\/\/#override (${id})\n([\\s\\S]*?)\n\/\/#endOverride`,
7274
);
75+
console.log(
76+
`Open-next plugin ${name} -- Applying override for ${id} from ${fp}`,
77+
);
7378
contents = contents.replace(pattern, replacement);
7479
}
7580
}),

packages/tests-e2e/tests/appRouter/isr.revalidate.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ test("Test revalidate", async ({ request }) => {
44
const result = await request.get("/api/isr");
55

66
expect(result.status()).toEqual(200);
7-
const body = await result.json();
7+
const json = await result.json();
8+
const body = json.body;
9+
10+
expect(json.status).toEqual(200);
811
expect(body.result).toEqual(true);
912
expect(body.cacheControl).toEqual(
1013
"private, no-cache, no-store, max-age=0, must-revalidate",

0 commit comments

Comments
 (0)