Repository navigation
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…
iki commentedon Oct 27, 2026
New downstream evidence from Windows x64, Bun 1.4.2, Wrangler 4.153.0 / Miniflare 5.20261023.0-beta, and installed Undici 8.12.3: our isolated tooling preload replaces Bun's bare
undicishim with the installed package and installs that package's actualfetchglobally. A loopback MockAgent control confirms native Bun fetch ignores the dispatcher while the installed implementation honors it; D1 access and Worker build checks pass. The local preview URL now returns the explicit 503 from #15846 because the guard tests runtime identity regardless of that replacement.We understand the policy in #15846 (review): fixing native Bun remains Bun's responsibility. Related work is oven-sh/bun#36102 (installed module resolution), oven-sh/bun#39250 and oven-sh/bun#38855 (dispatcher handling); oven-sh/bun#27338 explicitly excludes dispatcher-aware fetch.
For our downstream patch, we plan a narrow local-only exception: the preload first verifies its real fetch with a network-disabled MockAgent, then registers that exact function under
Symbol.for("wrangler.dispatcherFetch"). The local ProxyWorker and test-harness guards accept it only while the registered function is identical to current global fetch. Missing/stale registration retains the existing rejection; runtime identity is never spoofed. Remote bindings stay blocked and are outside this proposal. Regression coverage will exercise the unmodified rejection, verified override, replacement-after-verification rejection, real local HTTP dispatch and cleanup.Would an explicit capability registration like this be acceptable upstream, or should it remain downstream-only? We will keep the change atomic. I am not listed in VOUCHED.td, so this is the issue-first coordination requested by CONTRIBUTING.md before any PR. No upstream fix or successful patched-runtime result is claimed yet.
iki commentedon Oct 27, 2026
The downstream workaround is now published and tested: reuse instructions and package patch, on codex/bun-local-fetch-capability (signed head
1c0977d2f5485af9a65562f8fafeaa6b3a4124de).New results on Windows x64 with Bun 1.4.2, Wrangler 4.153.0, Undici 8.12.3 and Miniflare 5.20261023.0-beta:
503 !== 200), then passed local HTTP,server.fetch()andgetWorker().fetch("http://worker.invalid/test")with the patch and verified preload.The branch contains the source change, exact 4.153.0 package diff, portable preload and regression commands. The two prerequisite adapters also have separate atomic branches: installed Undici resolution and dispatcher-aware global fetch. The final branch includes both in dependency order.
This remains downstream-only in line with the policy discussed above. Remote bindings are still blocked. Registration is a trusted local contract, not a security boundary or official support; an already-created proxy binding is not a live monitor of later global changes. Keep the preload in a dedicated tooling process. The full Workers SDK source suite has not been run; the evidence is against the patched published package and our integration.
No PR opened. If maintainers are interested in this opt-in, I can prepare an atomic PR after the contributor-registration coordination.
workers-devprod commentedon Oct 28, 2026
Thanks for the careful coordination and for keeping the patch downstream-only in line with the policy discussed on #15846. We'd like it to stay that way: the guard exists because Bun's
fetch()ignores the undicidispatcherit routes through, and the real fix belongs with Bun (oven-sh/bun#39250) — once that ships, the guard becomes removable, and an upstream bypass would be an interface we'd have to maintain past that point. Please keep the capability registration in your fork for now, and feel free to open an issue if it's still needed once Bun's dispatcher support lands.iki commentedon Nov 6, 2026
Updated the downstream package patch for Wrangler 4.158.0, Miniflare 5.20261104.0-beta and Undici 8.12.4, tested on Windows x64 with Bun 1.4.2: exact patch, setup and regression commands (signed head
fc97183fd758fee0b74bdc92ba7da703976deff6).The same local HTTP and direct harness requests pass; missing/stale registration and fetch replacement still reject, and each harness closes. The portable source guard test also passes under Node 26.13.0. The capability contract is unchanged, and this remains downstream-only as requested.
For users following the newer Miniflare/Bun report in #16644, these instructions provide the tested package combination; they do not claim to fix or validate Bun 1.4.4, remote bindings, or the full Workers SDK source suite.