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
This commit is contained in:
Gustavo Valverde
2026-10-12 14:43:33 +01:00
parent fe2559b7a0
commit d70a2ad27d
5 changed files with 31 additions and 1 deletions
+5
View File
@@ -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.
+7
View File
@@ -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(
+1 -1
View File
@@ -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"
@@ -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;
}
+1
View File
@@ -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",