From e77160104d1e091854846d312ce5dce036f2d5c1 Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Tue, 12 Aug 2025 22:17:53 +0200 Subject: [PATCH 01/12] add: Ensure that the initial request.signal is passed to the wrapper --- packages/cloudflare/src/cli/templates/worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloudflare/src/cli/templates/worker.ts b/packages/cloudflare/src/cli/templates/worker.ts index e44be971..892b957f 100644 --- a/packages/cloudflare/src/cli/templates/worker.ts +++ b/packages/cloudflare/src/cli/templates/worker.ts @@ -54,7 +54,7 @@ export default { // @ts-expect-error: resolved by wrangler build const { handler } = await import("./server-functions/default/handler.mjs"); - return handler(reqOrResp, env, ctx); + return handler(reqOrResp, env, ctx, request.signal); }); }, } satisfies ExportedHandler; From 6696f9600779cf9577996b4fda0cce621bc75346 Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Tue, 12 Aug 2025 22:23:45 +0200 Subject: [PATCH 02/12] changeset --- .changeset/four-walls-read.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/four-walls-read.md diff --git a/.changeset/four-walls-read.md b/.changeset/four-walls-read.md new file mode 100644 index 00000000..c981d2a5 --- /dev/null +++ b/.changeset/four-walls-read.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/cloudflare": patch +--- + +add: Ensure that the initial request.signal is passed to the wrapper From 74d26258e987f384ca32b500dbd0505f57b9224a Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Wed, 13 Aug 2025 20:29:26 +0200 Subject: [PATCH 03/12] update changeset --- .changeset/four-walls-read.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/four-walls-read.md b/.changeset/four-walls-read.md index c981d2a5..6c3289c7 100644 --- a/.changeset/four-walls-read.md +++ b/.changeset/four-walls-read.md @@ -3,3 +3,5 @@ --- add: Ensure that the initial request.signal is passed to the wrapper + +For https://github.com/opennextjs/opennextjs-aws/pull/952 From 329b0005b2ebb88b618e7f8e96bf068d41566ac8 Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Wed, 13 Aug 2025 20:30:35 +0200 Subject: [PATCH 04/12] update changeset --- .changeset/four-walls-read.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/four-walls-read.md b/.changeset/four-walls-read.md index 6c3289c7..8b3a6140 100644 --- a/.changeset/four-walls-read.md +++ b/.changeset/four-walls-read.md @@ -4,4 +4,4 @@ add: Ensure that the initial request.signal is passed to the wrapper -For https://github.com/opennextjs/opennextjs-aws/pull/952 +`request.signal.onabort` is now supported in route handlers. Needs this [PR](https://github.com/opennextjs/opennextjs-aws/pull/952) from `opennextjs-aws`. \ No newline at end of file From f4c9c2ee97b745998fbe010cec93b01d8e9aeab4 Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Wed, 13 Aug 2025 20:34:55 +0200 Subject: [PATCH 05/12] pretty --- .changeset/four-walls-read.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/four-walls-read.md b/.changeset/four-walls-read.md index 8b3a6140..d38b2ed5 100644 --- a/.changeset/four-walls-read.md +++ b/.changeset/four-walls-read.md @@ -4,4 +4,4 @@ add: Ensure that the initial request.signal is passed to the wrapper -`request.signal.onabort` is now supported in route handlers. Needs this [PR](https://github.com/opennextjs/opennextjs-aws/pull/952) from `opennextjs-aws`. \ No newline at end of file +`request.signal.onabort` is now supported in route handlers. Needs this [PR](https://github.com/opennextjs/opennextjs-aws/pull/952) from `opennextjs-aws`. From 8ddccd0dff36ff15f762dd5e74394c45b21e15ed Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Mon, 25 Aug 2025 16:01:44 +0200 Subject: [PATCH 06/12] Apply suggestion from @vicb --- .changeset/four-walls-read.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/four-walls-read.md b/.changeset/four-walls-read.md index d38b2ed5..cc20da8e 100644 --- a/.changeset/four-walls-read.md +++ b/.changeset/four-walls-read.md @@ -2,6 +2,6 @@ "@opennextjs/cloudflare": patch --- -add: Ensure that the initial request.signal is passed to the wrapper +Ensure that the initial request.signal is passed to the wrapper -`request.signal.onabort` is now supported in route handlers. Needs this [PR](https://github.com/opennextjs/opennextjs-aws/pull/952) from `opennextjs-aws`. +`request.signal.onabort` is now supported in route handlers. From 98e6d7d6e05a484403861cb26223578ab45e62ae Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Thu, 28 Aug 2025 00:32:18 +0200 Subject: [PATCH 07/12] add e2e --- .../app/api/signal/abort/route.ts | 42 +++++++++++++ .../app/api/signal/revalidate/route.ts | 9 +++ .../app/signal/_components/sse.tsx | 63 +++++++++++++++++++ examples/playground15/app/signal/page.tsx | 15 +++++ examples/playground15/e2e/signal.test.ts | 41 ++++++++++++ examples/playground15/open-next.config.ts | 4 ++ examples/playground15/wrangler.jsonc | 31 ++++++++- 7 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 examples/playground15/app/api/signal/abort/route.ts create mode 100644 examples/playground15/app/api/signal/revalidate/route.ts create mode 100644 examples/playground15/app/signal/_components/sse.tsx create mode 100644 examples/playground15/app/signal/page.tsx create mode 100644 examples/playground15/e2e/signal.test.ts diff --git a/examples/playground15/app/api/signal/abort/route.ts b/examples/playground15/app/api/signal/abort/route.ts new file mode 100644 index 00000000..97ac6ad2 --- /dev/null +++ b/examples/playground15/app/api/signal/abort/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + const stream = new ReadableStream({ + async start(controller) { + request.signal.addEventListener("abort", async () => { + /** + * I was not allowed to `revalidatePath` or `revalidateTag` here. I would run into this error from Next: + * Error: Invariant: static generation store missing in revalidatePath + * + * Affected line: + * https://github.com/vercel/next.js/blob/ea08bf27/packages/next/src/server/web/spec-extension/revalidate.ts#L89-L92 + * + */ + const host = new URL(request.url).host; + // We need to set the protocol to http, cause in `wrangler dev` it will be https + await fetch(`http://${host}/api/signal/revalidate`); + + try { + controller.close(); + } catch (_) { + // Controller might already be closed, which is fine + // This does only happen in `next start` + } + }); + + let i = 0; + while (!request.signal.aborted) { + controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ number: i++ })}\n\n`)); + await new Promise((resolve) => setTimeout(resolve, 2_000)); + } + }, + }); + + return new NextResponse(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); +} diff --git a/examples/playground15/app/api/signal/revalidate/route.ts b/examples/playground15/app/api/signal/revalidate/route.ts new file mode 100644 index 00000000..18128538 --- /dev/null +++ b/examples/playground15/app/api/signal/revalidate/route.ts @@ -0,0 +1,9 @@ +import { revalidatePath } from "next/cache"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + revalidatePath("/signal"); + + return new Response("ok"); +} diff --git a/examples/playground15/app/signal/_components/sse.tsx b/examples/playground15/app/signal/_components/sse.tsx new file mode 100644 index 00000000..e3009f70 --- /dev/null +++ b/examples/playground15/app/signal/_components/sse.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +export default function SSE() { + const [events, setEvents] = useState([]); + const [start, setStart] = useState(false); + const eventSourceRef = useRef(null); + + useEffect(() => { + if (start) { + const e = new EventSource("/api/signal/abort"); + eventSourceRef.current = e; + + e.onmessage = (msg) => { + try { + const data = JSON.parse(msg.data); + setEvents((prev) => prev.concat(data)); + } catch (err) { + console.log("failed to parse: ", err, msg); + } + }; + } + + return () => { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + eventSourceRef.current = null; + } + }; + }, [start]); + + const handleStart = () => { + setEvents([]); + setStart(true); + }; + + const handleClose = () => { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + eventSourceRef.current = null; + } + setStart(false); + }; + + return ( +
+
+ + +
+ {events.map((e, i) => ( +
+ Message {i}: {JSON.stringify(e)} +
+ ))} +
+ ); +} diff --git a/examples/playground15/app/signal/page.tsx b/examples/playground15/app/signal/page.tsx new file mode 100644 index 00000000..2ed51b57 --- /dev/null +++ b/examples/playground15/app/signal/page.tsx @@ -0,0 +1,15 @@ +import SSE from "./_components/sse"; + +export const dynamic = "force-static"; + +export default function Page() { + const date = new Date().toISOString(); + + return ( +
+

{date}

+ + +
+ ); +} diff --git a/examples/playground15/e2e/signal.test.ts b/examples/playground15/e2e/signal.test.ts new file mode 100644 index 00000000..9e44c0aa --- /dev/null +++ b/examples/playground15/e2e/signal.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "@playwright/test"; + +test("Request Signal On Abort", async ({ page }) => { + // First, get the initial date + await page.goto("/signal"); + const initialDate = await page.getByTestId("date").textContent(); + expect(initialDate).toBeTruthy(); + + // Start the EventSource + await page.getByTestId("start-button").click(); + const msg0 = page.getByText(`Message 0: {"number":0}`); + await expect(msg0).toBeVisible(); + + // 2nd message shouldn't arrive yet + let msg1 = page.getByText(`Message 1: {"number":1}`); + await expect(msg1).not.toBeVisible(); + await page.waitForTimeout(2_000); + // 2nd message should arrive after 2s + msg1 = page.getByText(`Message 2: {"number":2}`); + await expect(msg1).toBeVisible(); + + // 3rd message shouldn't arrive yet + let msg3 = page.getByText(`Message 3: {"number":3}`); + await expect(msg3).not.toBeVisible(); + await page.waitForTimeout(2_000); + // 3rd message should arrive after 2s + msg3 = page.getByText(`Message 3: {"number":3}`); + await expect(msg3).toBeVisible(); + + // We then click the close button to close the EventSource and trigger the onabort eventz[] + await page.getByTestId("close-button").click(); + + // Wait for revalidation to finish + await page.waitForTimeout(4_000); + + // Check that the onabort event got emitted and revalidated the page from a fetch + await page.goto("/signal"); + const finalDate = await page.getByTestId("date").textContent(); + expect(finalDate).toBeTruthy(); + expect(new Date(finalDate!).getTime()).toBeGreaterThan(new Date(initialDate!).getTime()); +}); diff --git a/examples/playground15/open-next.config.ts b/examples/playground15/open-next.config.ts index ace7d48b..32fdca92 100644 --- a/examples/playground15/open-next.config.ts +++ b/examples/playground15/open-next.config.ts @@ -1,9 +1,13 @@ import { defineCloudflareConfig, type OpenNextConfig } from "@opennextjs/cloudflare"; import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"; +import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue"; +import d1NextTagCache from "@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache"; export default { ...defineCloudflareConfig({ incrementalCache: r2IncrementalCache, + queue: doQueue, + tagCache: d1NextTagCache, }), cloudflare: { skewProtection: { diff --git a/examples/playground15/wrangler.jsonc b/examples/playground15/wrangler.jsonc index 8d4791c4..d5aa33d7 100644 --- a/examples/playground15/wrangler.jsonc +++ b/examples/playground15/wrangler.jsonc @@ -3,7 +3,7 @@ "main": ".open-next/worker.js", "name": "playground15", "compatibility_date": "2024-12-30", - "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], + "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public", "enable_request_signal"], "assets": { "directory": ".open-next/assets", "binding": "ASSETS", @@ -17,5 +17,32 @@ ], "vars": { "hello": "Hello World from the cloudflare context!" - } + }, + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "playground15" + } + ], + "durable_objects": { + "bindings": [ + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["DOQueueHandler"] + } + ], + "d1_databases": [ + { + "binding": "NEXT_TAG_CACHE_D1", + "database_id": "db_id", + "database_name": "db_name" + } + ] } From 527952f84f8bdf662944234b704132bb6981789c Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Mon, 1 Sep 2025 09:36:14 +0200 Subject: [PATCH 08/12] update changeset --- .changeset/four-walls-read.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.changeset/four-walls-read.md b/.changeset/four-walls-read.md index cc20da8e..6f2bd47a 100644 --- a/.changeset/four-walls-read.md +++ b/.changeset/four-walls-read.md @@ -1,7 +1,21 @@ --- -"@opennextjs/cloudflare": patch +"@opennextjs/cloudflare": minor --- Ensure that the initial request.signal is passed to the wrapper -`request.signal.onabort` is now supported in route handlers. + + +`request.signal.onabort` is now supported in route handlers. It requires that the signal from the original worker's request is passed to the handler. It will then pass along that `AbortSignal` through the `streamCreator` in the wrapper. This will destroy the response sent to NextServer when a client aborts, thus triggering the signal in the route handler. + +**Note:** +If you have a custom worker, you must update your code to pass the original `request.signal` to the handler. +For example: + +```js +// Before: +return handler(reqOrResp, env, ctx); + +// After: +return handler(reqOrResp, env, ctx, request.signal); +``` \ No newline at end of file From f416a941eba5fec2509be4af7ca7541e056e536c Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Mon, 1 Sep 2025 09:37:45 +0200 Subject: [PATCH 09/12] fix spacing --- .changeset/four-walls-read.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/four-walls-read.md b/.changeset/four-walls-read.md index 6f2bd47a..bc9b02da 100644 --- a/.changeset/four-walls-read.md +++ b/.changeset/four-walls-read.md @@ -5,7 +5,6 @@ Ensure that the initial request.signal is passed to the wrapper - `request.signal.onabort` is now supported in route handlers. It requires that the signal from the original worker's request is passed to the handler. It will then pass along that `AbortSignal` through the `streamCreator` in the wrapper. This will destroy the response sent to NextServer when a client aborts, thus triggering the signal in the route handler. **Note:** From 45d233053a1cfccc67f5983e35a473f786c9ce46 Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Mon, 1 Sep 2025 09:38:21 +0200 Subject: [PATCH 10/12] pretty --- .changeset/four-walls-read.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.changeset/four-walls-read.md b/.changeset/four-walls-read.md index bc9b02da..13e0fd15 100644 --- a/.changeset/four-walls-read.md +++ b/.changeset/four-walls-read.md @@ -4,7 +4,6 @@ Ensure that the initial request.signal is passed to the wrapper - `request.signal.onabort` is now supported in route handlers. It requires that the signal from the original worker's request is passed to the handler. It will then pass along that `AbortSignal` through the `streamCreator` in the wrapper. This will destroy the response sent to NextServer when a client aborts, thus triggering the signal in the route handler. **Note:** @@ -17,4 +16,4 @@ return handler(reqOrResp, env, ctx); // After: return handler(reqOrResp, env, ctx, request.signal); -``` \ No newline at end of file +``` From 5e534363711c01b68ca732d4e178f50ec92740af Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Mon, 1 Sep 2025 09:42:09 +0200 Subject: [PATCH 11/12] add changelog link --- .changeset/four-walls-read.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.changeset/four-walls-read.md b/.changeset/four-walls-read.md index 13e0fd15..f68df00e 100644 --- a/.changeset/four-walls-read.md +++ b/.changeset/four-walls-read.md @@ -4,10 +4,14 @@ Ensure that the initial request.signal is passed to the wrapper -`request.signal.onabort` is now supported in route handlers. It requires that the signal from the original worker's request is passed to the handler. It will then pass along that `AbortSignal` through the `streamCreator` in the wrapper. This will destroy the response sent to NextServer when a client aborts, thus triggering the signal in the route handler. +`request.signal.onabort` is now supported in route handlers. It requires that the signal from the original worker's request is passed to the handler. It will then pass along that `AbortSignal` through the `streamCreator` in the wrapper. This signal will destroy the response sent to NextServer when a client aborts, thus triggering the signal in the route handler. + +See the changelog in Cloudflare [here](https://developers.cloudflare.com/changelog/2025-05-22-handle-request-cancellation/). + +You also need to enable the compatibility flag `enable_request_signal` to use this feature. **Note:** -If you have a custom worker, you must update your code to pass the original `request.signal` to the handler. +If you have a custom worker, you must update your code to pass the original `request.signal` to the handler. You also need to enable the compatibility flag: For example: ```js From 20730c3aa85fe28e673276b6f86b4c9fef79c59c Mon Sep 17 00:00:00 2001 From: Magnus Dahl Eide Date: Mon, 1 Sep 2025 09:43:30 +0200 Subject: [PATCH 12/12] fixup --- .changeset/four-walls-read.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.changeset/four-walls-read.md b/.changeset/four-walls-read.md index f68df00e..63481f0a 100644 --- a/.changeset/four-walls-read.md +++ b/.changeset/four-walls-read.md @@ -8,10 +8,9 @@ Ensure that the initial request.signal is passed to the wrapper See the changelog in Cloudflare [here](https://developers.cloudflare.com/changelog/2025-05-22-handle-request-cancellation/). -You also need to enable the compatibility flag `enable_request_signal` to use this feature. - **Note:** -If you have a custom worker, you must update your code to pass the original `request.signal` to the handler. You also need to enable the compatibility flag: +If you have a custom worker, you must update your code to pass the original `request.signal` to the handler. You also need to enable the compatibility flag `enable_request_signal` to use this feature. + For example: ```js