diff --git a/.changeset/chilly-dryers-begin.md b/.changeset/chilly-dryers-begin.md
new file mode 100644
index 00000000..5d2df349
--- /dev/null
+++ b/.changeset/chilly-dryers-begin.md
@@ -0,0 +1,30 @@
+---
+"@opennextjs/cloudflare": patch
+---
+
+enable `getCloudflareContext` to work in middlewares via a new `enableEdgeDevGetCloudflareContext` utility
+
+`getCloudflareContext` can't work, during development (with `next dev`), in middlewares since they run in
+the edge runtime (see: https://nextjs.org/docs/app/building-your-application/routing/middleware#runtime) which
+is incompatible with the `wrangler`'s APIs which run in node.js
+
+To solve this a new utility called `enableEdgeDevGetCloudflareContext` has been introduced that allows the
+context to be available also in the edge runtime, the utility needs to be called inside the Next.js config
+file, for example:
+
+```js
+// next.config.mjs
+
+import { enableEdgeDevGetCloudflareContext } from "@opennextjs/cloudflare";
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+enableEdgeDevGetCloudflareContext();
+
+export default nextConfig;
+```
+
+a helpful error is also thrown in `getCloudflareContext` to prompt developers to use the utility
+
+`getCloudflareContext` called in the nodejs runtime works as before without needing any setup
diff --git a/examples/middleware/app/middleware/page.tsx b/examples/middleware/app/middleware/page.tsx
index aa28fa5f..51739a0b 100644
--- a/examples/middleware/app/middleware/page.tsx
+++ b/examples/middleware/app/middleware/page.tsx
@@ -1,3 +1,25 @@
+import { headers } from "next/headers";
+
export default function MiddlewarePage() {
- return
Via middleware
;
+ const cloudflareContextHeader = headers().get("x-cloudflare-context");
+
+ return (
+ <>
+ Via middleware
+
+ The value of the x-cloudflare-context header is:
+
+ {cloudflareContextHeader}
+
+
+ >
+ );
}
diff --git a/examples/middleware/e2e/base.spec.ts b/examples/middleware/e2e/base.spec.ts
index 75ce9421..ec0f7565 100644
--- a/examples/middleware/e2e/base.spec.ts
+++ b/examples/middleware/e2e/base.spec.ts
@@ -3,28 +3,28 @@ import { test, expect } from "@playwright/test";
test("redirect", async ({ page }) => {
await page.goto("/");
await page.click('[href="/about"]');
- expect(page.waitForURL("**/redirected"));
+ await page.waitForURL("**/redirected");
expect(await page.textContent("h1")).toContain("Redirected");
});
test("rewrite", async ({ page }) => {
await page.goto("/");
await page.click('[href="/another"]');
- expect(page.waitForURL("**/another"));
+ await page.waitForURL("**/another");
expect(await page.textContent("h1")).toContain("Rewrite");
});
test("no matching middleware", async ({ page }) => {
await page.goto("/");
await page.click('[href="/about2"]');
- expect(page.waitForURL("**/about2"));
+ await page.waitForURL("**/about2");
expect(await page.textContent("h1")).toContain("About 2");
});
test("matching noop middleware", async ({ page }) => {
await page.goto("/");
await page.click('[href="/middleware"]');
- expect(page.waitForURL("**/middleware"));
+ await page.waitForURL("**/middleware");
expect(await page.textContent("h1")).toContain("Via middleware");
});
diff --git a/examples/middleware/e2e/cloudflare-context.spec.ts b/examples/middleware/e2e/cloudflare-context.spec.ts
new file mode 100644
index 00000000..bd34724c
--- /dev/null
+++ b/examples/middleware/e2e/cloudflare-context.spec.ts
@@ -0,0 +1,7 @@
+import { test, expect } from "@playwright/test";
+
+test("cloudflare context env object is populated", async ({ page }) => {
+ await page.goto("/middleware");
+ const a = page.getByTestId("cloudflare-context-header");
+ expect(await a.textContent()).toEqual("keys of `cloudflareContext.env`: MY_VAR, MY_KV, ASSETS");
+});
diff --git a/examples/middleware/e2e/playwright.config.ts b/examples/middleware/e2e/playwright.config.ts
index d6d8499c..8aa2d486 100644
--- a/examples/middleware/e2e/playwright.config.ts
+++ b/examples/middleware/e2e/playwright.config.ts
@@ -49,5 +49,7 @@ export default defineConfig({
command: "pnpm preview:worker",
url: "http://localhost:8774",
reuseExistingServer: !process.env.CI,
+ // the app takes a bit to boot up for some reason, that's why we need a longer timeout here
+ timeout: 2 * 60 * 1000,
},
});
diff --git a/examples/middleware/middleware.ts b/examples/middleware/middleware.ts
index 9145eb73..c432c77c 100644
--- a/examples/middleware/middleware.ts
+++ b/examples/middleware/middleware.ts
@@ -1,7 +1,9 @@
import { NextRequest, NextResponse, NextFetchEvent } from "next/server";
import { clerkMiddleware } from "@clerk/nextjs/server";
-export function middleware(request: NextRequest, event: NextFetchEvent) {
+import { getCloudflareContext } from "@opennextjs/cloudflare";
+
+export async function middleware(request: NextRequest, event: NextFetchEvent) {
console.log("middleware");
if (request.nextUrl.pathname === "/about") {
return NextResponse.redirect(new URL("/redirected", request.url));
@@ -16,9 +18,21 @@ export function middleware(request: NextRequest, event: NextFetchEvent) {
})(request, event);
}
- return NextResponse.next();
+ const requestHeaders = new Headers(request.headers);
+ const cloudflareContext = await getCloudflareContext();
+
+ requestHeaders.set(
+ "x-cloudflare-context",
+ `keys of \`cloudflareContext.env\`: ${Object.keys(cloudflareContext.env).join(", ")}`
+ );
+
+ return NextResponse.next({
+ request: {
+ headers: requestHeaders,
+ },
+ });
}
export const config = {
- matcher: ["/about/:path*", "/another/:path*", "/middleware/:path*", "/clerk"],
+ matcher: ["/about/:path*", "/another/:path*", "/middleware/:path*", "/clerk", "/cloudflare-context"],
};
diff --git a/examples/middleware/next.config.mjs b/examples/middleware/next.config.mjs
index 4678774e..12e2f394 100644
--- a/examples/middleware/next.config.mjs
+++ b/examples/middleware/next.config.mjs
@@ -1,4 +1,8 @@
+import { enableEdgeDevGetCloudflareContext } from "@opennextjs/cloudflare";
+
/** @type {import('next').NextConfig} */
const nextConfig = {};
+enableEdgeDevGetCloudflareContext();
+
export default nextConfig;
diff --git a/examples/middleware/wrangler.json b/examples/middleware/wrangler.json
index a0fb8497..1f761090 100644
--- a/examples/middleware/wrangler.json
+++ b/examples/middleware/wrangler.json
@@ -6,5 +6,9 @@
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
- }
+ },
+ "vars": {
+ "MY_VAR": "my-var"
+ },
+ "kv_namespaces": [{ "binding": "MY_KV", "id": "" }]
}
diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts
new file mode 100644
index 00000000..1e4d79b8
--- /dev/null
+++ b/packages/cloudflare/src/api/cloudflare-context.ts
@@ -0,0 +1,149 @@
+declare global {
+ interface CloudflareEnv {
+ NEXT_CACHE_WORKERS_KV?: KVNamespace;
+ ASSETS?: Fetcher;
+ }
+}
+
+export type CloudflareContext<
+ CfProperties extends Record = IncomingRequestCfProperties,
+ Context = ExecutionContext,
+> = {
+ /**
+ * the worker's [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/)
+ */
+ env: CloudflareEnv;
+ /**
+ * the request's [cf properties](https://developers.cloudflare.com/workers/runtime-apis/request/#the-cf-property-requestinitcfproperties)
+ */
+ cf: CfProperties | undefined;
+ /**
+ * the current [execution context](https://developers.cloudflare.com/workers/runtime-apis/context)
+ */
+ ctx: Context;
+};
+
+// Note: this symbol needs to be kept in sync with the one used in `src/cli/templates/worker.ts`
+const cloudflareContextSymbol = Symbol.for("__cloudflare-context__");
+
+/**
+ * Utility to get the current Cloudflare context
+ *
+ * @returns the cloudflare context
+ */
+export async function getCloudflareContext<
+ CfProperties extends Record = IncomingRequestCfProperties,
+ Context = ExecutionContext,
+>(): Promise> {
+ const global = globalThis as unknown as {
+ [cloudflareContextSymbol]: CloudflareContext | undefined;
+ };
+
+ const cloudflareContext = global[cloudflareContextSymbol];
+
+ if (!cloudflareContext) {
+ // the cloudflare context is initialized by the worker and is always present in production/preview,
+ // so, it not being present means that the application is running under `next dev`
+ return getCloudflareContextInNextDev();
+ }
+
+ return cloudflareContext;
+}
+
+const cloudflareContextInNextDevSymbol = Symbol.for("__next-dev/cloudflare-context__");
+
+/**
+ * Gets a local proxy version of the cloudflare context (created using `getPlatformProxy`) when
+ * running in the standard next dev server (via `next dev`)
+ *
+ * @returns the local proxy version of the cloudflare context
+ */
+async function getCloudflareContextInNextDev<
+ CfProperties extends Record = IncomingRequestCfProperties,
+ Context = ExecutionContext,
+>(): Promise> {
+ const global = globalThis as unknown as {
+ [cloudflareContextInNextDevSymbol]: CloudflareContext | undefined;
+ };
+
+ if (!global[cloudflareContextInNextDevSymbol]) {
+ try {
+ const cloudflareContext = await getCloudflareContextFromWrangler();
+ global[cloudflareContextInNextDevSymbol] = cloudflareContext;
+ } catch (e: unknown) {
+ if (e instanceof Error && e.message.includes("A dynamic import callback was not specified")) {
+ const getCloudflareContextFunctionName = getCloudflareContext.name;
+ const enablingFunctionName = enableEdgeDevGetCloudflareContext.name;
+ throw new Error(
+ `\n\n\`${getCloudflareContextFunctionName}\` has been invoked during development inside the edge runtime ` +
+ "this is not enabled, to enable such use of the function you need to import and call the " +
+ `\`${enablingFunctionName}\` function inside your Next.js config file\n\n"` +
+ "Example: \n ```\n // next.config.mjs\n\n" +
+ ` import { ${enablingFunctionName} } from "@opennextjs/cloudflare";\n\n` +
+ ` ${enablingFunctionName}();\n\n` +
+ " /** @type {import('next').NextConfig} */\n" +
+ " const nextConfig = {};\n" +
+ " export default nextConfig;\n" +
+ " ```\n" +
+ "\n(note: currently middlewares in Next.js are always run using the edge runtime)\n\n"
+ );
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ return global[cloudflareContextInNextDevSymbol]!;
+}
+
+type RuntimeContext = Record & {
+ process?: { env?: Record };
+ [cloudflareContextInNextDevSymbol]?: {
+ env: unknown;
+ ctx: unknown;
+ cf: unknown;
+ };
+};
+
+/**
+ * Enables `getCloudflareContext` to work, during development (via `next dev`), in the edge runtime
+ *
+ * Note: currently middlewares always run in the edge runtime
+ *
+ * Note: this function should only be called inside the Next.js config file
+ */
+export async function enableEdgeDevGetCloudflareContext() {
+ const require = (
+ await import(/* webpackIgnore: true */ `${"__module".replaceAll("_", "")}`)
+ ).default.createRequire(import.meta.url);
+
+ // eslint-disable-next-line unicorn/prefer-node-protocol -- the `next dev` compiler doesn't accept the node prefix
+ const vmModule = require("vm");
+
+ const originalRunInContext = vmModule.runInContext.bind(vmModule);
+
+ const context = await getCloudflareContextFromWrangler();
+
+ vmModule.runInContext = (...args: [string, RuntimeContext, ...unknown[]]) => {
+ const runtimeContext = args[1];
+ runtimeContext[cloudflareContextInNextDevSymbol] ??= context;
+ return originalRunInContext(...args);
+ };
+}
+
+async function getCloudflareContextFromWrangler<
+ CfProperties extends Record = IncomingRequestCfProperties,
+ Context = ExecutionContext,
+>(): Promise> {
+ // Note: we never want wrangler to be bundled in the Next.js app, that's why the import below looks like it does
+ const { getPlatformProxy } = await import(/* webpackIgnore: true */ `${"__wrangler".replaceAll("_", "")}`);
+ const { env, cf, ctx } = await getPlatformProxy({
+ // This allows the selection of a wrangler environment while running in next dev mode
+ environment: process.env.NEXT_DEV_WRANGLER_ENV,
+ });
+ return {
+ env,
+ cf: cf as unknown as CfProperties,
+ ctx: ctx as Context,
+ };
+}
diff --git a/packages/cloudflare/src/api/get-cloudflare-context.ts b/packages/cloudflare/src/api/get-cloudflare-context.ts
deleted file mode 100644
index 1e627309..00000000
--- a/packages/cloudflare/src/api/get-cloudflare-context.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-declare global {
- interface CloudflareEnv {
- NEXT_CACHE_WORKERS_KV?: KVNamespace;
- ASSETS?: Fetcher;
- }
-}
-
-export type CloudflareContext<
- CfProperties extends Record = IncomingRequestCfProperties,
- Context = ExecutionContext,
-> = {
- /**
- * the worker's [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/)
- */
- env: CloudflareEnv;
- /**
- * the request's [cf properties](https://developers.cloudflare.com/workers/runtime-apis/request/#the-cf-property-requestinitcfproperties)
- */
- cf: CfProperties | undefined;
- /**
- * the current [execution context](https://developers.cloudflare.com/workers/runtime-apis/context)
- */
- ctx: Context;
-};
-
-// Note: this symbol needs to be kept in sync with the one used in `src/cli/templates/worker.ts`
-const cloudflareContextSymbol = Symbol.for("__cloudflare-context__");
-
-/**
- * Utility to get the current Cloudflare context
- *
- * @returns the cloudflare context
- */
-export async function getCloudflareContext<
- CfProperties extends Record = IncomingRequestCfProperties,
- Context = ExecutionContext,
->(): Promise> {
- const global = globalThis as unknown as {
- [cloudflareContextSymbol]: CloudflareContext | undefined;
- };
-
- const cloudflareContext = global[cloudflareContextSymbol];
-
- if (!cloudflareContext) {
- // the cloudflare context is initialized by the worker and is always present in production/preview,
- // so, it not being present means that the application is running under `next dev`
- return getCloudflareContextInNextDev();
- }
-
- return cloudflareContext;
-}
-
-const cloudflareContextInNextDevSymbol = Symbol.for("__next-dev/cloudflare-context__");
-
-/**
- * Gets a local proxy version of the cloudflare context (created using `getPlatformProxy`) when
- * running in the standard next dev server (via `next dev`)
- *
- * @returns the local proxy version of the cloudflare context
- */
-async function getCloudflareContextInNextDev<
- CfProperties extends Record = IncomingRequestCfProperties,
- Context = ExecutionContext,
->(): Promise> {
- const global = globalThis as unknown as {
- [cloudflareContextInNextDevSymbol]: CloudflareContext | undefined;
- };
-
- if (!global[cloudflareContextInNextDevSymbol]) {
- // Note: we never want wrangler to be bundled in the Next.js app, that's why the import below looks like it does
- const { getPlatformProxy } = await import(
- /* webpackIgnore: true */ `${"__wrangler".replaceAll("_", "")}`
- );
- const { env, cf, ctx } = await getPlatformProxy({
- // This allows the selection of a wrangler environment while running in next dev mode
- environment: process.env.NEXT_DEV_WRANGLER_ENV,
- });
- global[cloudflareContextInNextDevSymbol] = {
- env,
- cf: cf as unknown as CfProperties,
- ctx: ctx as Context,
- };
- }
-
- return global[cloudflareContextInNextDevSymbol]!;
-}
diff --git a/packages/cloudflare/src/api/index.ts b/packages/cloudflare/src/api/index.ts
index 5442c415..574ce7de 100644
--- a/packages/cloudflare/src/api/index.ts
+++ b/packages/cloudflare/src/api/index.ts
@@ -1 +1 @@
-export * from "./get-cloudflare-context.js";
+export * from "./cloudflare-context.js";
diff --git a/packages/cloudflare/src/api/kvCache.ts b/packages/cloudflare/src/api/kvCache.ts
index 6e780a9b..5c2652cb 100644
--- a/packages/cloudflare/src/api/kvCache.ts
+++ b/packages/cloudflare/src/api/kvCache.ts
@@ -2,7 +2,7 @@ import type { KVNamespace } from "@cloudflare/workers-types";
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides";
import { IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js";
-import { getCloudflareContext } from "./get-cloudflare-context.js";
+import { getCloudflareContext } from "./cloudflare-context.js";
export const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";