Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/spicy-seas-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@opennextjs/cloudflare": patch
---

fix: deployed worker unable to invoke itself in memory queue

In deployments, Cloudflare Workers are unable to invoke workers on the same account via fetch, and the recommended way to call a worker is to use a service binding. This change switches to use service bindings for the memory queue to avoid issues with worker-to-worker subrequests.

To continue using the memory queue, add a service binding to your wrangler config for the binding `NEXT_CACHE_REVALIDATION_WORKER`.

```json
{
"services": [
{
"binding": "NEXT_CACHE_REVALIDATION_WORKER",
"service": "<WORKER_NAME>"
}
]
}
```
6 changes: 6 additions & 0 deletions examples/e2e/app-pages-router/wrangler.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@
"binding": "NEXT_CACHE_WORKERS_KV",
"id": "<BINDING_ID>"
}
],
"services": [
{
"binding": "NEXT_CACHE_REVALIDATION_WORKER",
"service": "app-pages-router"
}
]
}
6 changes: 6 additions & 0 deletions examples/e2e/app-router/wrangler.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@
"database_id": "NEXT_CACHE_D1",
"database_name": "NEXT_CACHE_D1"
}
],
"services": [
{
"binding": "NEXT_CACHE_REVALIDATION_WORKER",
"service": "app-router"
}
]
}
6 changes: 6 additions & 0 deletions examples/e2e/pages-router/wrangler.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@
"binding": "NEXT_CACHE_WORKERS_KV",
"id": "<BINDING_ID>"
}
],
"services": [
{
"binding": "NEXT_CACHE_REVALIDATION_WORKER",
"service": "pages-router"
}
]
}
1 change: 1 addition & 0 deletions packages/cloudflare/src/api/cloudflare-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ declare global {
NEXT_CACHE_D1?: D1Database;
NEXT_CACHE_D1_TAGS_TABLE?: string;
NEXT_CACHE_D1_REVALIDATIONS_TABLE?: string;
NEXT_CACHE_REVALIDATION_WORKER?: Service;
ASSETS?: Fetcher;
}
}
Expand Down
19 changes: 13 additions & 6 deletions packages/cloudflare/src/api/memory-queue.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { generateMessageGroupId } from "@opennextjs/aws/core/routing/queue.js";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";

import cache, { DEFAULT_REVALIDATION_TIMEOUT_MS } from "./memory-queue";
import cache, { DEFAULT_REVALIDATION_TIMEOUT_MS } from "./memory-queue.js";

vi.mock("./.next/prerender-manifest.json", () => Promise.resolve({ preview: { previewModeId: "id" } }));

const mockServiceWorkerFetch = vi.fn();
vi.mock("./cloudflare-context", () => ({
getCloudflareContext: () => ({
env: { NEXT_CACHE_REVALIDATION_WORKER: { fetch: mockServiceWorkerFetch } },
}),
}));

describe("MemoryQueue", () => {
beforeAll(() => {
vi.useFakeTimers();
Expand All @@ -21,7 +28,7 @@ describe("MemoryQueue", () => {
});
vi.advanceTimersByTime(DEFAULT_REVALIDATION_TIMEOUT_MS);
await firstRequest;
expect(globalThis.internalFetch).toHaveBeenCalledTimes(1);
expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(1);

const secondRequest = cache.send({
MessageBody: { host: "test.local", url: "/test" },
Expand All @@ -30,7 +37,7 @@ describe("MemoryQueue", () => {
});
vi.advanceTimersByTime(1);
await secondRequest;
expect(globalThis.internalFetch).toHaveBeenCalledTimes(2);
expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(2);
});

it("should process revalidations for multiple paths", async () => {
Expand All @@ -41,7 +48,7 @@ describe("MemoryQueue", () => {
});
vi.advanceTimersByTime(1);
await firstRequest;
expect(globalThis.internalFetch).toHaveBeenCalledTimes(1);
expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(1);

const secondRequest = cache.send({
MessageBody: { host: "test.local", url: "/test" },
Expand All @@ -50,7 +57,7 @@ describe("MemoryQueue", () => {
});
vi.advanceTimersByTime(1);
await secondRequest;
expect(globalThis.internalFetch).toHaveBeenCalledTimes(2);
expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(2);
});

it("should de-dupe revalidations", async () => {
Expand All @@ -68,6 +75,6 @@ describe("MemoryQueue", () => {
];
vi.advanceTimersByTime(1);
await Promise.all(requests);
expect(globalThis.internalFetch).toHaveBeenCalledTimes(1);
expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(1);
});
});
10 changes: 9 additions & 1 deletion packages/cloudflare/src/api/memory-queue.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import logger from "@opennextjs/aws/logger.js";
import type { Queue, QueueMessage } from "@opennextjs/aws/types/overrides.js";
import { IgnorableError } from "@opennextjs/aws/utils/error.js";

import { getCloudflareContext } from "./cloudflare-context";

export const DEFAULT_REVALIDATION_TIMEOUT_MS = 10_000;

/**
* The Memory Queue offers basic ISR revalidation by directly requesting a revalidation of a route.
*
* It offers basic support for in-memory de-duping per isolate.
*
* A service binding called `NEXT_CACHE_REVALIDATION_WORKER` that points to your worker is required.
*/
export class MemoryQueue implements Queue {
readonly name = "memory-queue";
Expand All @@ -16,6 +21,9 @@ export class MemoryQueue implements Queue {
constructor(private opts = { revalidationTimeoutMs: DEFAULT_REVALIDATION_TIMEOUT_MS }) {}

async send({ MessageBody: { host, url }, MessageGroupId }: QueueMessage): Promise<void> {
const service = getCloudflareContext().env.NEXT_CACHE_REVALIDATION_WORKER;
if (!service) throw new IgnorableError("No service binding for cache revalidation worker");

if (this.revalidatedPaths.has(MessageGroupId)) return;

this.revalidatedPaths.set(
Expand All @@ -30,7 +38,7 @@ export class MemoryQueue implements Queue {
// TODO: Drop the import - https://github.com/opennextjs/opennextjs-cloudflare/issues/361
// @ts-ignore
const manifest = await import("./.next/prerender-manifest.json");
await globalThis.internalFetch(`${protocol}://${host}${url}`, {
await service.fetch(`${protocol}://${host}${url}`, {
method: "HEAD",
headers: {
"x-prerender-revalidate": manifest.preview.previewModeId,
Expand Down