You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
wrangler dev: TypeError: Incoming CONNECT on a worker not supported (thrown synchronously from connect())
getPlatformProxy() (vite dev): surfaces as TypeError: This ReadableStream is disturbed (has already been read from), and cannot be used as a body., with the underlying workerd log showing the same root error: workerd/api/sockets.c++:449: error: Socket proxy disconnected abruptly; e = workerd/io/worker-entrypoint.c++:648: failed: jsg.TypeError: Incoming CONNECT on a worker not supported
fetch() on the same binding relays correctly in both modes. The same connect() call works when the Worker is deployed (TCP connect() over VPC Networks, GA 2026-06-16).
Root cause (traced to source)
Remote bindings are wired locally as a JS proxy worker, and that worker cannot receive the native CONNECT that Fetcher#connect() produces:
Miniflare registers every remote-capable binding (including vpc_networks) as a workerd Service.worker running packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts, which implements fetch() and capnweb JSRPC relay only — there is no connect handler (packages/miniflare/src/plugins/vpc-networks/index.ts → remoteProxyClientWorker() in packages/miniflare/src/plugins/shared/constants.ts).
env.BINDING.connect(addr) is a native (C++) code path: workerd turns it into an HTTP CONNECT delivered to that proxy worker's entrypoint.
WorkerEntrypoint::connect rejects inbound CONNECT unless the receiving worker has the connect_pass_through or experimental compat flag (workerd/src/workerd/io/worker-entrypoint.c++:648). The proxy client worker has neither, so the call dies locally — the request never reaches the remote proxy session.
The edge side (packages/wrangler/templates/remoteBindings/ProxyServerWorker.ts) also only handles fetch/JSRPC; there is no socket relay path.
Note the existing remote-bindings e2e test for VPC Networks only exercises .fetch() (and is currently skipped), so this raw-TCP gap has no coverage.
Feasibility evidence (we prototyped each building block)
We ran three spikes on main to check a fix is viable without any workerd changes:
Local ingress works. A miniflare worker with compatibilityFlags: ["experimental"] and an export default { connect(socket) } handler (workerd's inbound connect handler, merged in EW-9330 Implement connect() handler workerd#6059) successfully receives a service-binding connect() from another worker, with bidirectional bytes. Without the per-worker flag it fails with exactly the error above (the global --experimental CLI flag miniflare already passes is not sufficient — the check is per-worker).
The target address is recoverable. The caller's connect("host:port") authority arrives verbatim as (await socket.opened).localAddress in the handler (matching the contract exercised by workerd/src/workerd/api/tests/connect-handler-test.js), so no extra address-negotiation protocol is needed.
The edge side already works. We added a temporary diagnostic branch to ProxyServerWorker.ts that calls env[binding].connect(addr) inside a real remote proxy session (started via startRemoteProxySession() with a real vpc_network binding + Cloudflare Tunnel): the connect succeeded and returned the first 78 bytes of a genuine MySQL handshake from a private-network database. So connect() on the raw binding is fully functional in the preview session context.
Proposed fix (we're happy to submit a PR)
remote-proxy-client.worker.ts: add a connect(socket) handler; recover the target address from socket.opened.localAddress; relay the byte stream to the remote proxy session over a WebSocket (binary frames), following the auth/headers conventions of the existing fetch/JSRPC relay.
ProxyServerWorker.ts: add a WebSocket tunnel endpoint that calls await env[binding].connect(address) and pipes both directions, propagating close/error.
plugins/shared/constants.ts / plugins/vpc-networks: opt the proxy client worker into the experimental compat flag (scoped to bindings that need raw TCP), since the inbound connect handler is still experimental-gated in workerd.
Open questions we'd like maintainer input on before sending the PR:
Is enabling the experimental compat flag on miniflare's internal proxy-client worker acceptable, given the inbound connect handler (workerd#6059) is not yet stable? If not, would you prefer this gated behind an --experimental-* wrangler flag per the naming conventions in CONTRIBUTING.md?
Should the relay reuse the existing capnweb WebSocket session or open a dedicated WebSocket per socket? (Our prototype assumes a dedicated WS per connect() for simplicity.)
wrangler dev → curl → {"ok":false,"error":"TypeError: Incoming CONNECT on a worker not supported"}.
The same Worker deployed → {"ok":true,"bytes":78} (MySQL greeting via Cloudflare Tunnel).
Please provide a link to a minimal reproduction
The snippets above are the minimal reproduction. A self-contained repro repository is not practical here because reproducing requires an account-specific Cloudflare Tunnel and a private database behind it.
Please provide any relevant log output
✘ [ERROR] workerd/server/server.c++:...: error: Uncaught: workerd/io/worker-entrypoint.c++:648: failed: jsg.TypeError: Incoming CONNECT on a worker not supported
workerd/api/sockets.c++:449: error: Socket proxy disconnected abruptly
Which Cloudflare product(s) does this pertain to?
Wrangler, Miniflare (remote bindings)
What versions are you using?
main@c7dbe1a)What operating system and mode are you using?
macOS,
wrangler dev(local mode with remote bindings) andgetPlatformProxy()(via@sveltejs/adapter-cloudflare)Describe the Bug
What happened
With a
vpc_networksbinding configured withremote: true, calling the binding's raw-TCP API from a Worker in local dev fails immediately:wrangler dev:TypeError: Incoming CONNECT on a worker not supported(thrown synchronously fromconnect())getPlatformProxy()(vite dev): surfaces asTypeError: This ReadableStream is disturbed (has already been read from), and cannot be used as a body., with the underlying workerd log showing the same root error:workerd/api/sockets.c++:449: error: Socket proxy disconnected abruptly; e = workerd/io/worker-entrypoint.c++:648: failed: jsg.TypeError: Incoming CONNECT on a worker not supportedfetch()on the same binding relays correctly in both modes. The sameconnect()call works when the Worker is deployed (TCPconnect()over VPC Networks, GA 2026-06-16).Root cause (traced to source)
Remote bindings are wired locally as a JS proxy worker, and that worker cannot receive the native CONNECT that
Fetcher#connect()produces:vpc_networks) as a workerdService.workerrunningpackages/miniflare/src/workers/shared/remote-proxy-client.worker.ts, which implementsfetch()and capnweb JSRPC relay only — there is noconnecthandler (packages/miniflare/src/plugins/vpc-networks/index.ts→remoteProxyClientWorker()inpackages/miniflare/src/plugins/shared/constants.ts).env.BINDING.connect(addr)is a native (C++) code path: workerd turns it into an HTTP CONNECT delivered to that proxy worker's entrypoint.WorkerEntrypoint::connectrejects inbound CONNECT unless the receiving worker has theconnect_pass_throughorexperimentalcompat flag (workerd/src/workerd/io/worker-entrypoint.c++:648). The proxy client worker has neither, so the call dies locally — the request never reaches the remote proxy session.packages/wrangler/templates/remoteBindings/ProxyServerWorker.ts) also only handles fetch/JSRPC; there is no socket relay path.Note the existing remote-bindings e2e test for VPC Networks only exercises
.fetch()(and is currently skipped), so this raw-TCP gap has no coverage.Feasibility evidence (we prototyped each building block)
We ran three spikes on
mainto check a fix is viable without any workerd changes:compatibilityFlags: ["experimental"]and anexport default { connect(socket) }handler (workerd's inbound connect handler, merged in EW-9330 Implement connect() handler workerd#6059) successfully receives a service-bindingconnect()from another worker, with bidirectional bytes. Without the per-worker flag it fails with exactly the error above (the global--experimentalCLI flag miniflare already passes is not sufficient — the check is per-worker).connect("host:port")authority arrives verbatim as(await socket.opened).localAddressin the handler (matching the contract exercised byworkerd/src/workerd/api/tests/connect-handler-test.js), so no extra address-negotiation protocol is needed.ProxyServerWorker.tsthat callsenv[binding].connect(addr)inside a real remote proxy session (started viastartRemoteProxySession()with a realvpc_networkbinding + Cloudflare Tunnel): the connect succeeded and returned the first 78 bytes of a genuine MySQL handshake from a private-network database. Soconnect()on the raw binding is fully functional in the preview session context.Proposed fix (we're happy to submit a PR)
remote-proxy-client.worker.ts: add aconnect(socket)handler; recover the target address fromsocket.opened.localAddress; relay the byte stream to the remote proxy session over a WebSocket (binary frames), following the auth/headers conventions of the existing fetch/JSRPC relay.ProxyServerWorker.ts: add a WebSocket tunnel endpoint that callsawait env[binding].connect(address)and pipes both directions, propagating close/error.plugins/shared/constants.ts/plugins/vpc-networks: opt the proxy client worker into theexperimentalcompat flag (scoped to bindings that need raw TCP), since the inbound connect handler is still experimental-gated in workerd.Open questions we'd like maintainer input on before sending the PR:
experimentalcompat flag on miniflare's internal proxy-client worker acceptable, given the inbound connect handler (workerd#6059) is not yet stable? If not, would you prefer this gated behind an--experimental-*wrangler flag per the naming conventions in CONTRIBUTING.md?connect()for simplicity.)Reproduction
Minimal repro:
wrangler dev→ curl →{"ok":false,"error":"TypeError: Incoming CONNECT on a worker not supported"}.The same Worker deployed →
{"ok":true,"bytes":78}(MySQL greeting via Cloudflare Tunnel).Please provide a link to a minimal reproduction
The snippets above are the minimal reproduction. A self-contained repro repository is not practical here because reproducing requires an account-specific Cloudflare Tunnel and a private database behind it.
Please provide any relevant log output