Skip to content

Commit 8384743

Browse files
authored
[vitest-pool-workers] Fix dynamic import() in entrypoint and DO handlers (cloudflare#13056)
1 parent 53ed15a commit 8384743

11 files changed

Lines changed: 139 additions & 8 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@cloudflare/vitest-pool-workers": patch
3+
---
4+
5+
fix: Support dynamic `import()` inside entrypoint and Durable Object handlers
6+
7+
Previously, calling `exports.default.fetch()` or `SELF.fetch()` on a worker whose handler used a dynamic `import()` would hang and fail with "Cannot perform I/O on behalf of a different Durable Object". This happened because the module runner's transport — which communicates over a WebSocket owned by the runner Durable Object — was invoked from a different DO context.
8+
9+
The fix patches the module runner's transport via the `onModuleRunner` hook so that all `invoke()` calls are routed through the runner DO's I/O context, regardless of where the `import()` originates.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function greet(name: string): string {
2+
return `Hello, ${name}!`;
3+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { DurableObject } from "cloudflare:workers";
2+
3+
// Durable Object that uses dynamic import() in fetch handler.
4+
// Regression test for https://github.com/cloudflare/workers-sdk/issues/5387
5+
export class GreeterDO extends DurableObject {
6+
async fetch(request: Request): Promise<Response> {
7+
const { greet } = await import("./greeting");
8+
return new Response(greet("DO"));
9+
}
10+
}
11+
12+
export default {
13+
async fetch(
14+
request: Request,
15+
_env: unknown,
16+
_ctx: ExecutionContext
17+
): Promise<Response> {
18+
// Dynamic import inside a fetch handler — this is the pattern that
19+
// triggers the cross-DO I/O violation in vitest-pool-workers 0.13.x
20+
// when called via `exports.default.fetch()` in tests.
21+
// See: https://github.com/cloudflare/workers-sdk/issues/12924
22+
const { greet } = await import("./greeting");
23+
return new Response(greet("World"));
24+
},
25+
} satisfies ExportedHandler;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../tsconfig.workerd.json",
3+
"include": ["./**/*.ts"]
4+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
declare namespace Cloudflare {
2+
interface GlobalProps {
3+
mainModule: typeof import("./index");
4+
}
5+
interface Env {
6+
GREETER: DurableObjectNamespace;
7+
}
8+
}
9+
interface Env extends Cloudflare.Env {}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { env } from "cloudflare:workers";
2+
import { exports } from "cloudflare:workers";
3+
import { it } from "vitest";
4+
5+
// Regression test for https://github.com/cloudflare/workers-sdk/issues/12924
6+
//
7+
// Calling exports.default.fetch() on a worker whose fetch handler uses a
8+
// dynamic import() would hang with "Cannot perform I/O on behalf of a
9+
// different Durable Object". Pre-loading the module (e.g. via a static
10+
// import of the worker) masks the bug by caching the module.
11+
it("exports.default.fetch() with dynamic import()", async ({ expect }) => {
12+
const response = await exports.default.fetch(
13+
new Request("https://example.com/")
14+
);
15+
expect(response.status).toBe(200);
16+
expect(await response.text()).toBe("Hello, World!");
17+
});
18+
19+
// Regression test for https://github.com/cloudflare/workers-sdk/issues/5387
20+
//
21+
// Dynamic import() inside a Durable Object fetch handler has the same
22+
// cross-context I/O violation when the module isn't already cached.
23+
it("Durable Object fetch with dynamic import()", async ({ expect }) => {
24+
const id = env.GREETER.idFromName("test");
25+
const stub = env.GREETER.get(id);
26+
const response = await stub.fetch(new Request("https://example.com/"));
27+
expect(response.status).toBe(200);
28+
expect(await response.text()).toBe("Hello, DO!");
29+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../tsconfig.workerd-test.json",
3+
"include": ["./**/*.ts", "../src/**/*.ts"]
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../tsconfig.node.json",
3+
"include": ["./*.ts"]
4+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
2+
import { defineConfig } from "vitest/config";
3+
4+
export default defineConfig({
5+
plugins: [
6+
cloudflareTest({
7+
wrangler: {
8+
configPath: "./wrangler.jsonc",
9+
},
10+
}),
11+
],
12+
13+
test: {},
14+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "dynamic-import",
3+
"main": "src/index.ts",
4+
// don't provide compatibility_date so that vitest will infer the latest one
5+
"durable_objects": {
6+
"bindings": [{ "name": "GREETER", "class_name": "GreeterDO" }],
7+
},
8+
"migrations": [{ "tag": "v1", "new_classes": ["GreeterDO"] }],
9+
}

0 commit comments

Comments
 (0)