#9907 Cloudflare Vitest Pool Workers fails to handle 302 redirect responses from Durable Objects
Valid bug in vitest-pool-workers. actionResults Map assertion fails when DO returns 302 redirect. Root cause: module-level shared Map loses callback reference when redirect triggers new fetch context.
Keep open for maintainer fix. Proposed solution in report: add redirect: 'manual' to internal fetch or improve callback lookup graceful handling.
Analysis Report
Issue Review: cloudflare/workers-sdk#9907
Summary
@cloudflare/vitest-pool-workers fails with "Expected callback for X" error when a Durable Object returns a 302 redirect response inside runInDurableObject().
Findings
- Created: 2025-07-09
- Updated: 2025-07-21
- Version: @cloudflare/vitest-pool-workers 0.8.38 -> 0.12.6 (current)
- Component: vitest-pool-workers (Durable Object testing)
- Labels: bug, vitest
- Comments: 0
Key Evidence
- No related fix found - Searched PRs for issue #9907, "redirect", "Expected callback", and "runInDurableObject" - no merged PRs address this issue
- No changelog mention - Changelog does not reference issue #9907 or redirect-related fixes
- Root cause identified in source code - The bug is in
packages/vitest-pool-workers/src/worker/durable-objects.ts
Root Cause Analysis
The issue stems from how runInDurableObject() handles Response objects when multiple Durable Object instances are involved in the same isolate.
The Problem Flow:
- User calls
runInDurableObject(stub, callback)wherecallbackreturns a Response (e.g., 302 redirect) runInStub()(line 91-108) stores the callback inactionResultsMap with a uniqueactionId- It calls
stub.fetch()with a specialcfproperty containing theactionId - The wrapper's
fetch()method inentrypoints.tscallsmaybeHandleRunRequest() maybeHandleRunRequest()(line 186-212) retrieves the callback fromactionResultsusing theactionId
The Bug (durable-objects.ts:196):
const callback = actionResults.get(actionId);
assert(typeof callback === "function", `Expected callback for ${actionId}`);
The assertion fails when:
- The
actionIdin the request doesn't match any entry in theactionResultsMap - This happens because
actionResultsis a module-level global Map shared across all Durable Object instances - When a Durable Object makes a fetch to another Durable Object (or when the Response involves a redirect that triggers another fetch), the
actionIdmay not be found in the Map
Why 302 redirects specifically trigger this:
When a Response with a 302 status is returned, the test framework or underlying runtime may automatically follow the redirect. If this redirect triggers another stub.fetch() call:
- The new fetch doesn't have the original
actionIdstored inactionResults - Or the redirect goes to a different Durable Object that has its own separate context
- The
maybeHandleRunRequest()receives anactionIdthat was registered in a different context
Code References:
packages/vitest-pool-workers/src/worker/durable-objects.ts:9-actionResultsMap definitionpackages/vitest-pool-workers/src/worker/durable-objects.ts:91-108-runInStub()functionpackages/vitest-pool-workers/src/worker/durable-objects.ts:186-212-maybeHandleRunRequest()functionpackages/vitest-pool-workers/src/worker/entrypoints.ts:329-340- Wrapperfetch()method
Recommendation
Status: KEEP OPEN
Reasoning: This is a legitimate bug in the vitest-pool-workers package. The source code analysis confirms the issue exists in the callback handling mechanism. No fix has been merged, and the issue affects OAuth flows and redirect-based functionality testing.
Action: Keep open for maintainer attention. The issue is well-documented with reproduction steps and a clear workaround provided.
Proposed Solution
Option 1: Prevent automatic redirect following in test fetch calls (Recommended)
Modify runInStub() to explicitly disable redirect following:
// packages/vitest-pool-workers/src/worker/durable-objects.ts
async function runInStub<O extends DurableObject, R>(
stub: Fetcher,
callback: (instance: O, state: DurableObjectState) => R | Promise<R>
): Promise<R> {
const id = nextActionId++;
actionResults.set(id, callback);
const response = await stub.fetch("http://x", {
cf: { [CF_KEY_ACTION]: id },
redirect: "manual", // Prevent automatic redirect following
});
// ... rest of function
}
Option 2: More robust actionId validation
Add better error handling and skip the action lookup for redirect responses:
// packages/vitest-pool-workers/src/worker/durable-objects.ts
export async function maybeHandleRunRequest(
request: Request,
instance: unknown,
state?: DurableObjectState
): Promise<Response | undefined> {
const actionId = request.cf?.[CF_KEY_ACTION];
if (actionId === undefined) {
return;
}
assert(typeof actionId === "number", `Expected numeric ${CF_KEY_ACTION}`);
try {
const callback = actionResults.get(actionId);
// Gracefully handle missing callbacks instead of asserting
if (typeof callback !== "function") {
// This can happen if the actionId is stale or from a different context
// Return undefined to let the request pass through to user code
return;
}
// ... rest of function
} catch (e) {
actionResults.set(actionId, e);
return new Response(null, { status: 500 });
}
}
Option 3: Context-scoped action tracking
Use a WeakMap keyed by the Durable Object instance to scope action IDs:
const instanceActionResults = new WeakMap<object, Map<number, unknown>>();
function getActionResultsForInstance(instance: object): Map<number, unknown> {
let results = instanceActionResults.get(instance);
if (!results) {
results = new Map();
instanceActionResults.set(instance, results);
}
return results;
}
Implementation Details
Difficulty: Medium
Justification:
- The fix requires understanding the complex interaction between
runInDurableObject(), the wrapper fetch handler, and how Responses are passed back - Option 1 is simplest but may change behavior for users who expect redirects to be followed
- Option 2 changes error handling semantics
- Option 3 is more invasive but provides better isolation
- Need to ensure the fix doesn't break existing functionality for non-redirect Response types
Files to Modify:
packages/vitest-pool-workers/src/worker/durable-objects.ts- Main fix locationpackages/vitest-pool-workers/test/- Add test cases for redirect responses from Durable Objects
Testing Recommendations:
- Add unit test for
runInDurableObject()with 302 redirect response - Add unit test for
runInDurableObject()with 301 redirect response - Add integration test with a Durable Object that returns redirect to another Durable Object
- Add test verifying existing Response handling still works (200, 204, 500 responses)
- Test both
redirect: "follow"andredirect: "manual"behaviors
Notes & Feedback (0)
No notes yet.