Repository navigation
[wrangler] createTestHarness() under Bun: listen() succeeds, then server.fetch() and every request to the returned URL hang forever with no error #15717
Description
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.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
workers-devprod commentedon Sep 18, 2026
Automated triage
Issue assessment
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
Evidence and attempted steps
Blocker: Automated reproduction failed before recording a result.
Agent outcome: Failed.
Failure type:
submission_timeoutFailure: Submission exceeded the configured timeout.
Details: The operation ran longer than the configured durability timeout.
Submission ID:
sub_ik_631d346c60753641f8e4cdf8befe0b49This 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.dario-piotrowicz commentedon Sep 24, 2026
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) 🙏
oddharsh commentedon Sep 24, 2026
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(), thengetWorker().getEnv()). With the guard inlisten(), that path now throws too:listen()getWorker().fetch("/")server.fetch("/")Would it work to keep
listen()resolving, haveserver.fetch()throw the same UserError when theplaywasn'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 coversfetch(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.oddharsh commentedon Oct 1, 2026
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 intoDispatchFetchDispatcher. Bun'sfetchignoresdispatcher(oven-sh/bun#39247, fix still open as #39250), so under bungetWorker().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:
getWorker().getEnv()getWorker().fetch("/")getWorker().fetch("http://made-up.invalid/x")getWorker().fetch("http://example.com/x")server: cloudflare)server.fetch("/")fetch(url)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). EverydispatchFetch-based path now fails, and one of those failures sends traffic to a real host. So the cleaner shape might be:listen()keeps resolving, andgetEnv()keeps working.getWorker().fetch()andserver.fetch()both throw the Fail early whencreateTestHarness()cannot start its proxy under Bun #15845UserErrorunder bun, since they share the dispatcher door now. Checkingprocess.versions.bunin miniflare'sdispatchFetch()would cover both from one place.fetch(url)from test code.I can put that up against #15845's branch, or as a follow-up, whichever you prefer.
mylescarrick commentedon Oct 9, 2026
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.macOS arm64, bun 1.4.2 vs node v26.7.0:
env.VPC.fetch()env.VPC.fetch()put/getround-trips in under ~1.2s), so it's specific to the remote path.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
DispatchFetchDispatcherand an undiciPool, so I'd guess it's the same oven-sh/bun#39247 cause. Only testedvpc_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
getPlatformProxywith remote bindings, since this path currently gives no signal at all.oddharsh commentedon Oct 18, 2026
Thanks, that's a useful second path. #15846 guards only
createTestHarness()'s dispatch helper and the ProxyWorker, so it doesn't covergetPlatformProxy()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 sogetPlatformProxy({ remoteBindings: true })fails the same way under Bun, with a test.[remote-bindings] Fail remote proxy startup under Bun instead of hang…