Skip to content

[wrangler] createTestHarness() under Bun: listen() succeeds, then server.fetch() and every request to the returned URL hang forever with no error #15717

Description

@oddharsh

What versions & operating system are you using?

wrangler 4.131.2 (also the 2026-09-15 main build, pkg.pr.new f9e7727), miniflare 5.20260820.0-alpha
bun 1.4.2 / node v26.8.2
macOS 26 arm64

Please provide a link to a minimal reproduction

Three files, inline (happy to push them as a repo if that is easier):

// worker.js
export default { fetch: () => new Response("ok") };
// wrangler.jsonc
{ "name": "harness-probe", "main": "worker.js", "compatibility_date": "2026-09-01" }
// probe.mjs
import { createTestHarness } from "wrangler";
const t0 = Date.now(); const step = (s) => console.log(`[${Date.now() - t0}ms] ${s}`);
step(`runtime=${process.versions.bun ? "bun " + process.versions.bun : "node " + process.version}`);
const server = createTestHarness({ workers: [{ configPath: "./wrangler.jsonc" }] });
const { url } = await server.listen(); step(`listen -> ${url}`);
const w = await server.getWorker().fetch("/"); step(`getWorker().fetch("/") -> ${w.status}`);
const race = (p, label) => Promise.race([
  p.then(async (r) => step(`${label} -> ${r.status}`)),
  new Promise((res) => setTimeout(() => { step(`${label} -> no response after 5s`); res(); }, 5000)),
]);
await race(server.fetch("/"), 'server.fetch("/")');
await race(fetch(url), `fetch(url)`);
await server.close(); process.exit(0);

Describe the Bug

Run node probe.mjs and bun probe.mjs:

node 26 bun 1.4.2
listen() 211ms, returns a URL 204ms, returns a URL
getWorker().fetch("/") 200 200
server.fetch("/") 200 no response, ever
fetch(url) from the test 200 no response, ever

Under Bun nothing throws, nothing is logged, and the process only ends when the caller's own timeout fires. In a real suite that reads as every route timing out: our route oracle (168 routes through the harness) reports 167 hard failures in 9m23s under bun and 168 passes in 6s under node, with identical config.

Cause, as far as I can trace it: the URL from listen() and server.fetch() both go through the ProxyWorker, which queues requests until the ProxyController delivers play with the upstream in cf.hostMetadata. That is sent through miniflare's dispatchFetch(), which attaches the blob as MF-CF-Blob from a custom undici Dispatcher and calls fetch(url, { dispatcher }). Bun's fetch ignores the dispatcher option (oven-sh/bun#39247, fix pending in oven-sh/bun#39250), so the header never lands, request.cf?.hostMetadata is undefined in the ProxyWorker, and the queue never flushes. getWorker().fetch() works because it dispatches to the runtime miniflare directly with the route on MF-Route-Override, no ProxyWorker in the path. Delivering the play by hand under bun makes the same harness flush and serve, which is what convinced me it is the door rather than the config.

Wrangler already knows it does not support Bun (logPossibleBugMessage prints "Wrangler does not support the Bun runtime" when a command errors), but that message only fires on an error, and this path never produces one.

Suggestion: refuse up front. If process.versions.bun is set, have createTestHarness() (or listen()) throw a UserError naming the reason and pointing at bun#39247, the same way wrangler check startup refuses. A hang with no message is the worst version of unsupported. I am happy to open the PR with a test if that shape is acceptable; I held off because the right place for the guard (construction vs listen()) and whether to warn or throw felt like your call, and because the guard becomes removable once bun#39250 ships.

Please provide any relevant error logs

None. That is the bug.

Activity

  1. workers-devprod commented on Sep 18, 2026

    @workers-devprod
    Contributor

    Automated triage

    Issue assessment

    • Actionability: ✅ Actionable.
    • Type: Bug.
    • Reproduction: Automatic attempt requested.
    • Labels:
      • package:wrangler (high)
      • package:miniflare (medium)
      • error-messaging (medium)
      • contribution (low)
    Actionability and missing information

    Under Bun, wrangler's createTestHarness(): listen() succeeds but server.fetch() and fetch(url) hang forever with no error; getWorker().fetch() returns 200. Traced to Bun ignoring undici dispatcher option in miniflare dispatchFetch. Versions, repro, steps, and expected/actual behavior provided.

    Reproduction reason: The report provides a minimal three-file inline probe, exact versions, and a node-versus-bun result table isolating the silent hang to Bun, so the behavior is directly checkable.

    Reproduction

    • Result: ⚠️ Blocked.
    • Summary: Automated reproduction failed before recording a result.
    Evidence and attempted steps

    Blocker: Automated reproduction failed before recording a result.

    Agent outcome: Failed.

    Failure type: submission_timeout

    Failure: Submission exceeded the configured timeout.

    Details: The operation ran longer than the configured durability timeout.

    Submission ID: sub_ik_631d346c60753641f8e4cdf8befe0b49


    This summary was generated by an LLM and may contain errors. Please verify its findings.

    Triage bot commands and options

    Repository collaborators with write or admin access can trigger actions by commenting on this issue:

    • @workers-devprod reproduce: Attempt to reproduce the issue using its description and recent comments.
  2. moved this from Untriaged to In Progress in workers-sdkon Sep 24, 2026
  3. dario-piotrowicz commented on Sep 24, 2026

    @dario-piotrowicz
    Member

    Thanks for the issue @oddharsh 🙂

    I've implemented #15845 as an early fail for this (I wanted to implement it like this instead of flat out refusing to run under Bun just because there's always a possibility that this behavior will be fixed in Bun itself, right?)

    Please let me know what you think and if possible please also try the PR's prerelease at #15845 (comment) 🙏

  4. oddharsh commented on Sep 24, 2026

    @oddharsh
    ContributorAuthor

    Thanks for turning this around so fast, and the 400-on-missing-hostMetadata detection is neat: it fails in about 600ms rather than waiting out the timeout.

    I tried the prerelease (4.138.0) under bun 1.4.3, and it trips over something my original suggestion got wrong. getWorker().fetch() never goes through the ProxyWorker, so it works under bun today, and cal's suite in our repo relies on it (listen(), then getWorker().getEnv()). With the guard in listen(), that path now throws too:

    4.136.0 #15845
    listen() URL throws UserError
    getWorker().fetch("/") 200 unreachable
    server.fetch("/") hangs unreachable

    Would it work to keep listen() resolving, have server.fetch() throw the same UserError when the play wasn't acknowledged, and have the ProxyWorker answer with a 503 naming oven-sh/bun#39247 once it has seen a control request with no hostMetadata? That covers fetch(url) from test code too, which no wrapper can intercept, and it leaves the in-process door working. Happy to put up that change if it helps.

  5. oddharsh commented on Oct 1, 2026

    @oddharsh
    ContributorAuthor

    Following up on my last comment: the in-process path I said works under bun stopped working at #15906 (merged 2026-09-29). That PR moved dispatchFetch()'s rewrite onto the runtime origin into DispatchFetchDispatcher. Bun's fetch ignores dispatcher (oven-sh/bun#39247, fix still open as #39250), so under bun getWorker().fetch() now goes out with the URL exactly as the test wrote it.

    Same probe as the issue, bun 1.4.3, with node 26.10.0 as the control:

    bun, 4.143.0 (3bdcd0d) bun, 4.146.0 (b4954c1) node, 4.146.0
    getWorker().getEnv() works works works
    getWorker().fetch("/") 200 404 "No entrypoint worker found" 200
    getWorker().fetch("http://made-up.invalid/x") 200 ENOTFOUND 200
    getWorker().fetch("http://example.com/x") 200 404 from example.com itself (server: cloudflare) 200
    server.fetch("/") hangs hangs 200
    fetch(url) hangs hangs 200

    The third row is the one that matters. Under bun, a test that calls getWorker().fetch("https://my-app.example/api/reset") now sends that request to the real host instead of the Worker under test, with no error. #15906 fixes a real body-length bug under node, so I'd keep it and widen the guard in #15845 instead.

    So I'd revise my earlier suggestion. Under bun, getEnv() is now the only door that still works, and it's the one cal's suite uses (it calls its own handlers with the env). Every dispatchFetch-based path now fails, and one of those failures sends traffic to a real host. So the cleaner shape might be:

    • listen() keeps resolving, and getEnv() keeps working.
    • getWorker().fetch() and server.fetch() both throw the Fail early when createTestHarness() cannot start its proxy under Bun #15845 UserError under bun, since they share the dispatcher door now. Checking process.versions.bun in miniflare's dispatchFetch() would cover both from one place.
    • The ProxyWorker's 503 still covers fetch(url) from test code.

    I can put that up against #15845's branch, or as a follow-up, whichever you prefer.

  6. mylescarrick commented on Oct 9, 2026

    @mylescarrick

    Another path that hangs the same way under Bun: remote bindings via getPlatformProxy({ remoteBindings: true }).

    The proxy itself comes up fine, but every fetch() on a remote binding never settles, no error, no log. Same script under Node works.

    // wrangler.jsonc
    {
      "name": "vpc-probe",
      "main": "noop.js",
      "compatibility_date": "2026-09-01",
      "vpc_services": [{ "binding": "VPC", "service_id": "<vpc-service-id>", "remote": true }]
    }
    // probe.mjs
    import { getPlatformProxy } from "wrangler";
    const t0 = performance.now();
    const p = await getPlatformProxy({ configPath: "./wrangler.jsonc", remoteBindings: true });
    console.log("proxy ready", Math.round(performance.now() - t0), "ms");
    const r = await p.env.VPC.fetch("https://internal-host.example/");
    console.log("status", r.status);
    await p.dispose();

    macOS arm64, bun 1.4.2 vs node v26.7.0:

    wrangler (miniflare) bun: proxy ready bun: env.VPC.fetch() node: env.VPC.fetch()
    4.123.0 (5.20260811.1-alpha) ~2.5s hangs (>90s) —
    4.148.0 (5.20261006.0-alpha) ~3.4s hangs (>90s) —
    4.149.0 (5.20261006.1-alpha) ~5.4s hangs (>90s) ~0.4s, upstream response
    • GET, POST without a body, and POST with a JSON body all hang the same way.
    • Local-only bindings are fine under Bun on all three versions (KV put/get round-trips in under ~1.2s), so it's specific to the remote path.
    • Unlike the getWorker().fetch() change you noted after fix(miniflare): preserve request body length in dispatchFetch #15906, this isn't version-dependent: it already hangs on 4.123.0.

    I haven't traced it end to end, but from the miniflare source the binding proxy goes through DispatchFetchDispatcher and an undici Pool, so I'd guess it's the same oven-sh/bun#39247 cause. Only tested vpc_services; other remote binding types may or may not behave the same.

    If #15845's early-fail lands, it'd be great if it also covered getPlatformProxy with remote bindings, since this path currently gives no signal at all.

  7. oddharsh commented on Oct 18, 2026

    @oddharsh
    ContributorAuthor

    Thanks, that's a useful second path. #15846 guards only createTestHarness()'s dispatch helper and the ProxyWorker, so it doesn't cover getPlatformProxy() yet. If remote bindings go through miniflare's dispatcher as your reading of the source suggests, it's the same oven-sh/bun#39247 cause. I'll extend #15846 so getPlatformProxy({ remoteBindings: true }) fails the same way under Bun, with a test.

  8. moved this from In Progress to Done in workers-sdkon Oct 19, 2026
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

    package:wranglerRelating to the `wrangler` package

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions