Conversation
… doors The guard in cloudflare#15845 ran inside listen(), so under Bun it also refused server.getWorker().fetch(), which never goes through the ProxyWorker and worked before. Move the check to server.fetch(), and have the ProxyWorker answer with a 503 once a control request arrives without its payload, so requests to the listen() URL fail loudly instead of queueing forever.
|
| Name | Type |
|---|---|
| wrangler | Patch |
| @cloudflare/vite-plugin | Patch |
| @cloudflare/vitest-plugin | Patch |
Click here to learn what changesets are, and how to add one.
Click here if you're a maintainer who wants to add a changeset to this PR
@cloudflare/autoconfig
@cloudflare/build-output-utils
@cloudflare/codemods
@cloudflare/config
@cloudflare/containers-shared
create-cloudflare
@cloudflare/deploy-helpers
@cloudflare/kv-asset-handler
miniflare
@cloudflare/pages-functions
@cloudflare/pages-shared
@cloudflare/runtime-types
@cloudflare/unenv-preset
@cloudflare/vite-plugin
@cloudflare/vitest-plugin
@cloudflare/workers-auth
@cloudflare/workers-editor-shared
@cloudflare/workers-utils
wrangler
commit: |
…er the harness under simulated Bun A request already forwarded to the UserWorker sits in neither queue when an empty control request drains them, and its retry is requeued asynchronously once the old connection fails. Route both requeue sites through requeueForRetry(), which answers with the same 503 while controlPayloadMissing is set instead of waiting for a play that cannot come. Add a createTestHarness() test that simulates Bun under Node (isBun() true, proxy control requests stripped of cf) and asserts listen() and getWorker().fetch() work while server.fetch() throws and the listen() URL answers 503.
| if (this.controlPayloadMissing) { | ||
| return controlPayloadMissingResponse(); |
There was a problem hiding this comment.
🔴 Listening URL hangs when control disappears
When a control request never reaches the proxy, listen() returns a URL whose requests wait indefinitely. controlPayloadMissing only changes on an arriving request, so no 503 drains the queue.
Learn more
The proxy holds incoming URL requests in requestQueue until it receives a play control request. sendMessageToProxyWorker can finish unsuccessfully without delivering any control request. Since listen() no longer waits for its acknowledgment, the session and listening URL remain available in that case. The new flag is set only inside processProxyControllerRequest, which cannot run if the control request never arrives. The URL therefore keeps pending requests in the queue with no response; the Bun guard on server.fetch() does not cover direct requests to that URL.
Example: A Bun session completes its Worker reload, but all attempts to deliver play fail before reaching the ProxyWorker. listen() returns its URL; fetch(url) enters requestQueue and waits forever instead of returning the advertised 503.
Recommended fix: Handle unsuccessful or timed-out control delivery for requests arriving over the listening URL, including the case where no control message enters the Durable Object. Add coverage that drops the entire control request, not just its cf payload.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Not changed: a control request that never reaches the ProxyWorker is a pre-existing path on main for every runtime (after 4 attempts DevEnv.handleErrorEvent logs Failed to send message to ProxyWorker at debug and the URL waits), while Bun's failure is the request arriving without cf (measured: it gets the 400), which this PR covers; a queue timeout would also break requests held across slow rebuilds in wrangler dev, so I'd leave it to a separate change if maintainers want one.
🤖 Addressed by Claude Code
Fixes #15717.
This is stacked on #15845 and targets its branch, so it reads as a change to that PR. Merge it in or take the idea, whichever is easier.
#15845 turns the Bun hang into an error, which is the right call. It puts the guard in
listen(), though, and under Bun that also refusesserver.getWorker().fetch(). That door dispatches straight to Miniflare withMF-Route-Override, never touches the ProxyWorker, and works under Bun today. Our suite (bun testovercreateTestHarness(), callinglisten()and thengetWorker().getEnv()) breaks on the #15845 prerelease.This keeps #15845's detection and moves where it fires:
listen()resolves under Bun again, sogetWorker()keeps working.server.fetch()waits for the latestplayacknowledgement under Bun and throws the sameUserError, reworded to point atserver.getWorker().fetch().cf.hostMetadataand answers queued and later requests with a 503 naming undici shim:Poollacksdispatch()/close()/destroy(), breaking miniflare + workerd (wrangler dev, @cloudflare/vite-plugin) oven-sh/bun#39247. A control request that does carry its payload clears that state. This coversfetch(url)from test code, which no harness wrapper can intercept.Measured with this branch built, using the probe from the issue plus
fetch(url):listen()getWorker().fetch("/")server.fetch("/")UserError, 1ms aftergetWorker()fetch(url)On #15845 as it stands, Bun throws at
listen()and never reaches the other three rows.The new
ProxyControllertest boots a real ProxyWorker under Node and sends a control request the way Bun delivers it:Authorizationset, nocf. As a control, the same test against #15845's ProxyWorker times out at 15s, which reproduces the original hang without needing Bun. Two more tests came out of review:requeueForRetry()now gives it the 503 too. The test drops the connection of an in-flight request after a pause and an empty control request; without the fix it hangs to the 15s timeout.createTestHarness()test simulates Bun under Node (isBun()true, control requests stripped ofcf) and asserts all four doors from the table. It fails against Fail early whencreateTestHarness()cannot start its proxy under Bun #15845's harness, and it also fails if thecfstripping is removed, so the simulation is what makes it pass.pnpm check:lint,pnpm -F wrangler check:type(templates included) andpnpm check:formatall pass, as do all 14 test files undersrc/__tests__/api.server.fetch()JSDoc now describes the Bun behaviour