Workers SDK Issue Reports

← Back to Dashboard

#9907 Cloudflare Vitest Pool Workers fails to handle 302 redirect responses from Durable Objects

Recommendation:KEEP OPEN
Difficulty:medium
Reasoning:

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.

Suggested Action:

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

  1. No related fix found - Searched PRs for issue #9907, "redirect", "Expected callback", and "runInDurableObject" - no merged PRs address this issue
  2. No changelog mention - Changelog does not reference issue #9907 or redirect-related fixes
  3. 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:

  1. User calls runInDurableObject(stub, callback) where callback returns a Response (e.g., 302 redirect)
  2. runInStub() (line 91-108) stores the callback in actionResults Map with a unique actionId
  3. It calls stub.fetch() with a special cf property containing the actionId
  4. The wrapper's fetch() method in entrypoints.ts calls maybeHandleRunRequest()
  5. maybeHandleRunRequest() (line 186-212) retrieves the callback from actionResults using the actionId

The Bug (durable-objects.ts:196):

const callback = actionResults.get(actionId);
assert(typeof callback === "function", `Expected callback for ${actionId}`);

The assertion fails when:

  • The actionId in the request doesn't match any entry in the actionResults Map
  • This happens because actionResults is 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 actionId may 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:

  1. The new fetch doesn't have the original actionId stored in actionResults
  2. Or the redirect goes to a different Durable Object that has its own separate context
  3. The maybeHandleRunRequest() receives an actionId that was registered in a different context

Code References:

  • packages/vitest-pool-workers/src/worker/durable-objects.ts:9 - actionResults Map definition
  • packages/vitest-pool-workers/src/worker/durable-objects.ts:91-108 - runInStub() function
  • packages/vitest-pool-workers/src/worker/durable-objects.ts:186-212 - maybeHandleRunRequest() function
  • packages/vitest-pool-workers/src/worker/entrypoints.ts:329-340 - Wrapper fetch() 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:

  1. packages/vitest-pool-workers/src/worker/durable-objects.ts - Main fix location
  2. packages/vitest-pool-workers/test/ - Add test cases for redirect responses from Durable Objects

Testing Recommendations:

  1. Add unit test for runInDurableObject() with 302 redirect response
  2. Add unit test for runInDurableObject() with 301 redirect response
  3. Add integration test with a Durable Object that returns redirect to another Durable Object
  4. Add test verifying existing Response handling still works (200, 204, 500 responses)
  5. Test both redirect: "follow" and redirect: "manual" behaviors

Notes & Feedback (0)

No notes yet.

Add Note