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",