From d70a2ad27dff0f0fedbbb13bc3b90e0a6fc6ed01 Mon Sep 17 00:00:00 2001 From: Gustavo Valverde Date: Mon, 12 Oct 2026 14:32:52 +0100 Subject: [PATCH] fix(core): import AsyncLocalStorage statically on Cloudflare Workers The workerd entry of @better-auth/core/async_hooks loaded node:async_hooks through a module-scope dynamic import. When workerd cancels the request that evaluates the module, that import never settles, and every later request in the isolate waits on it. workerd exposes AsyncLocalStorage only as the node:async_hooks module, not on globalThis, so the entry now imports it statically. Closes #10315 --- .changeset/workerd-static-async-hooks.md | 5 +++++ e2e/smoke/test/cloudflare.spec.ts | 7 +++++++ packages/core/package.json | 2 +- packages/core/src/async_hooks/workerd.index.ts | 17 +++++++++++++++++ packages/core/tsdown.config.ts | 1 + 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/workerd-static-async-hooks.md create mode 100644 packages/core/src/async_hooks/workerd.index.ts diff --git a/.changeset/workerd-static-async-hooks.md b/.changeset/workerd-static-async-hooks.md new file mode 100644 index 0000000000..de5092ccdd --- /dev/null +++ b/.changeset/workerd-static-async-hooks.md @@ -0,0 +1,5 @@ +--- +"@better-auth/core": patch +--- + +On Cloudflare Workers, auth requests no longer hang in an isolate after the request that first loaded Better Auth was canceled. Before, every later request in that isolate waited forever until the isolate was recycled. No config change is needed; Workers still need the `nodejs_compat` or `nodejs_als` compatibility flag. diff --git a/e2e/smoke/test/cloudflare.spec.ts b/e2e/smoke/test/cloudflare.spec.ts index d7ca77ad86..ead3da227e 100644 --- a/e2e/smoke/test/cloudflare.spec.ts +++ b/e2e/smoke/test/cloudflare.spec.ts @@ -113,6 +113,13 @@ describe("(cloudflare) simple server", () => { "node:module", ]); assertContentDoesNotInclude("index.js", indexJs, unexpectedContents); + // A module-scope import("node:async_hooks") never settles if the request evaluating it is canceled. + assert.doesNotMatch( + indexJs, + /import\(\s*(?:\/\*[^]*?\*\/\s*)*"node:async_hooks"\s*\)/, + "index.js should import node:async_hooks statically", + ); + assert.match(indexJs, /from "node:async_hooks"/); const rolldownRuntime = await fs.readFile( join( diff --git a/packages/core/package.json b/packages/core/package.json index 4e3d049846..4b508f81bd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -54,7 +54,7 @@ "node": "./dist/async_hooks/index.mjs", "deno": "./dist/async_hooks/index.mjs", "bun": "./dist/async_hooks/index.mjs", - "workerd": "./dist/async_hooks/index.mjs", + "workerd": "./dist/async_hooks/workerd.index.mjs", "edge": "./dist/async_hooks/pure.index.mjs", "browser": "./dist/async_hooks/pure.index.mjs", "default": "./dist/async_hooks/index.mjs" diff --git a/packages/core/src/async_hooks/workerd.index.ts b/packages/core/src/async_hooks/workerd.index.ts new file mode 100644 index 0000000000..fb75100ec3 --- /dev/null +++ b/packages/core/src/async_hooks/workerd.index.ts @@ -0,0 +1,17 @@ +/** + * Cloudflare Workers entry. `nodejs_compat` (or `nodejs_als`) provides + * `node:async_hooks` as a module, not as a global, so it is imported + * statically: a module-scope dynamic import evaluated inside a request + * never settles once workerd cancels that request. + * + * @see https://github.com/better-auth/better-auth/issues/10315 + */ +import { AsyncLocalStorage } from "node:async_hooks"; + +export type { AsyncLocalStorage }; + +export async function getAsyncLocalStorage(): Promise< + typeof AsyncLocalStorage +> { + return AsyncLocalStorage; +} diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index cb68b41574..f02dced6fd 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -15,6 +15,7 @@ export default defineConfig({ "./src/db/adapter/index.ts", "./src/async_hooks/index.ts", "./src/async_hooks/pure.index.ts", + "./src/async_hooks/workerd.index.ts", "./src/context/index.ts", "./src/env/index.ts", "./src/oauth2/index.ts",