Skip to content

dev server returns 500 Internal Server Error on concurrent requests #3993

Description

@alexdupre

What is the location of your example repository?

No response

Which package or tool is having this issue?

Oxygen

What version of that package or tool are you using?

mini-oxygen 4.2.2

What version of React Router 7 are you using?

No response

Steps to Reproduce

  1. Launch the mini-oxygen dev server: npm run dev
  2. Launch 8 parallel/concurrent fetch of any endpoint, for example /robots.txt
for i in $(seq 1 8); do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/robots.txt & done; wait

Expected Behavior

All parallel requests return 200

Actual Behavior

Many parallel requests return 500, with the body:

Error: The script will never generate a response.
    at async Object.fetch (.../node_modules/miniflare/dist/src/workers/core/entry.worker.js:1029:22)

Cause

Vite's ModuleRunner de-duplicates concurrent module resolution by caching the in-flight promise, in @shopify/mini-oxygen/dist/vite/worker-entry.js:

async cachedModule(url, importer) {
  let cached = this.concurrentModuleNodePromises.get(url);
  if (cached) { /* reuse it */ }
  else {
    cached = this.getModuleInformation(url, importer, cachedModule)
      .finally(() => { this.concurrentModuleNodePromises.delete(url); });
    this.concurrentModuleNodePromises.set(url, cached);
  }
  return cached;
}

The runner is a module-scope singleton (var runtime in the same file), so that Map is shared by every request in the isolate. Request A starts getModuleInformation, whose I/O belongs to A's invocation, and parks the pending promise in the Map. Request B arrives while it is pending, takes A's promise and awaits it, and the Workers runtime forbids sharing I/O across invocations, so B's promise is severed, its handler never settles, and the entry worker's await service.fetch(request) rejects with the message above.

Upstream

vitejs/vite#20283 "ModuleRunner - Sharing concurrentModuleNodePromises across invocations is not compatible with Cloudflare Workers runtime".
Closed as p3-downstream-blocker with no fix in Vite, so the cache is still there in the runner mini-oxygen bundles.
Related: cloudflare/workers-sdk#9518.

Cloudflare fixed it on their side in cloudflare/workers-sdk#12953 (merged March 2026), touching one file: packages/vite-plugin-cloudflare/src/workers/runner-worker/module-runner.ts. They had a CustomModuleRunner subclass overriding cachedModule() with exactly this promise cache; the PR deletes the subclass and the cache, and instead intercepts ssrDynamicImportKey at evaluation time so every dynamic import runs through a Durable Object's IoContext (runInRunnerObject). That keeps de-duplication safe by giving the module I/O one long-lived owning context, rather than borrowing whichever request happened to arrive first, and it uses only public Vite APIs.

Patches

I used Claude Code to generate two possible patches, take them as prototypes, they both fixed the issue in two quite different ways.

@shopify+mini-oxygen+4.2.2.patch This patch neutralize the map so it always misses; each invocation then resolves in its own I/O context. Cost is one extra transport round-trip when two requests genuinely race on the same module, and nothing otherwise. Simple but it does reach into a field Vite marks private.

@shopify+mini-oxygen+4.2.2.public-api.patch This patch implements the same approach as Cloudflare: a Durable Object has one I/O context for every event it receives, so promises
created inside it are freely shareable. This keeps Vite's de-duplication rather than defeating it: concurrent requests still share one resolution, and it is now legally owned. More complete, but it brings durable objects and rpc into dev runtime.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions