Repository navigation
[miniflare] Support remote Hyperdrive bindings in local dev - #14900
Conversation
🦋 Changeset detectedLatest commit: 3c9053f The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Codeowners approval required for this PR:
Show detailed file reviewers
|
@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: |
147ec12 to
429ba95
Compare
|
Thanks — both are fair. Fixed the first; here's my reasoning on the second. Empty connection string / cryptic error ( I also covered the adjacent case Devin didn't flag: when the session is unavailable but a
That said, if you'd prefer a stronger guard — a per-session nonce, or folding the seed into the JSRPC surface rather than a plain header — I'm happy to add it. Also fixed two unrelated CI failures: the changeset listed |
|
@xortive this came out of your note on #14712 that the same relay would probably work for Hyperdrive Since you're on Hyperdrive, you'd be the right person to sanity-check two things:
The caveat: pointing a Hyperdrive designator at a Worker service segfaults workerd (cloudflare/workerd#6901 — confirmed from a source build), so this keeps the designator as CI is green, and a stock |
429ba95 to
cd71b5b
Compare
|
Codeowners approval required for this PR:
Show detailed file reviewers
|
|
Gentle status ping — no urgency, just keeping this from going stale. Since the last update this branch has been rebased onto
On the CI red: the four That leaves |
7d7e049 to
7f90236
Compare
|
Codeowners approval required for this PR:
Show detailed file reviewers
|
|
Rebased onto The rewrite is a simplification. The new structure already carries Also fixed while here: All four Devin findings from the previous round are marked resolved. @emily-shen — you triaged #14842 and you wrote the config migration this branch just adapted to, so you're better placed than most to say whether I landed it in the right shape. Two things worth a second opinion: whether resolving the binding through On CI: one job is red, |
a998533 to
30cfb6b
Compare
|
Correcting my last comment: the red What gave it away was comparing against other open PRs rather than against my own history — that job is green on all of them, and it had failed on three of my four commits. The mechanism turned out not to be slow startup ( The seeding step there was guarded only on CI is fully green for the first time — 50/50, including the Windows job that had been red. Worth noting for review: this was a real cost this PR imposed on unrelated dev sessions, and it was only visible under Windows timing. If you'd rather the seeding hang off a narrower seam than Also rebased onto current |
fa6cbe8 to
56e4ea6
Compare
|
Codeowners approval required for this PR:
Show detailed file reviewers
|
edmundhung
left a comment
There was a problem hiding this comment.
Sorry for the late review. This touches a lot of the area so it will take some time to make sure everything is addressed.
I have also asked the hyperdrive team to review this change.
4a8a99b to
70a0efb
Compare
|
All four comments are addressed inline. Two findings are worth surfacing here as well. The feature had regressed since #14994. The new v4→v5 conversion maps a Hyperdrive binding to Per-connection credentials (detailed in the thread above): worth a Hyperdrive-side opinion on whether that's the intended contract, or whether credentials are meant to be stable for a binding's lifetime. The fix works either way, but if it's the latter the seeding endpoint was built on a wrong assumption and it'd be worth saying so in the code. On CI: the three |
288f3be to
7dfb050
Compare
Client disconnects and shutdown must also close outbound TCP, TLS, and fallback sockets, including sockets from retired listeners. Preserve the local TLS cleanup from #14900 alongside the shared-session transport. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
Preserve the local TLS cleanup from #14900: client disconnects and shutdown also close outbound TCP, TLS, and fallback sockets, including connections from retired listeners. Co-authored-by: Henry Jang <jang@hyochan.site>
dde2026 to
8fd130d
Compare
|
Codeowners approval required for this PR:
Show detailed file reviewers |
|
This pull request was not reviewed automatically because its author does not have write access to this repository. A maintainer with write access can start a review by commenting |
With remote: true, application code still runs locally and uses an ordinary database driver, but its connections reach the deployed Hyperdrive binding. For example, pg.Client can use env.DB.connectionString and run SELECT 1 without knowing about remote bindings. Miniflare implements this shared path; Wrangler and Vite only pass through the configuration and resolve local fallbacks. The local Hyperdrive binding needs a TCP destination, whereas remote bindings provide remote calls (RPC) and streams over WebSocket. A bridge listening on 127.0.0.1 joins those interfaces without changing the driver or workerd. The existing local remote-session gateway forwards its WebSocket to a proxy Worker on Cloudflare, which holds the real Hyperdrive binding. Neither the bridge nor the proxy interprets SQL: they carry the driver's normal database traffic. During startup, the bridge opens a persistent RPC session and asks the proxy Worker for the binding's connection string. Those details configure the local binding before the local runtime starts. remoteProxyConnectionString names the gateway URL, not the database URL. When a driver connects, the bridge calls connect() on the same remote handle and copies bytes between the local socket and the returned streams. Three driver connections get three independent database sockets, not one shared database connection. Startup retries are bounded. A failed bridge is retained across unchanged reloads so one unavailable binding does not delay every file save. Restart development or change its target to retry. Losing an established session still allows a fresh session on the next reload. Configuration assembly awaits startup before rolling back, so shutdown cancellation is sufficient; no per-update abort signal is needed. The shared handle matters because credentials from remote binding instance A may not authenticate a socket opened by instance B. Fetching a connection string separately from opening sockets can cross that boundary. One session owns both operations instead of seeding credentials from a throwaway connection. - `ProxyServerWorker.ts`: The remote adapter exposes the native binding's connection string and a fresh connect() stream pair through the same handle. - `hyperdrive/index.ts`: Worker bindings route their synthetic database address to the TCP bridge. Node bindings instead expose its loopback address and port; both receive the session's user, password, and database together. - `hyperdrive-proxy.ts`: Each remote listener owns its session and accepted clients. An unchanged target reuses it. A successful reload retires unused listeners without interrupting existing connections; a failed reload rolls back new candidates. Shutdown cancels startup and closes active and retired resources. The existing local database/TLS proxy remains a separate connection path. - `remote-affinity.spec.ts`: Different credentials for each simulated session make accidental cross-session reuse fail. Worker and Node clients exercise reloads, database ID changes, reconnection, and another binding's failure without retrying that unchanged failed binding on each update. Co-authored-by: Henry Jang <jang@hyochan.site>
The existing local TLS proxy can still be waiting for a database greeting or TLS handshake when its client disconnects. Closing the listener alone does not close that outbound connection, especially after a reload retires the listener. Give each accepted client ownership of its outbound TCP, TLS, and fallback sockets before waiting for them. Client disconnects and shutdown close the whole connection. A closed peer rejects pending reads and handshake waits, and cannot trigger a new plaintext fallback after its client has gone away. PostgreSQL startup bytes may arrive while the proxy waits for the database's SSL response. Consume a bounded startup buffer so client EOF remains visible, then replay those bytes in order once TLS or plaintext fallback is ready. - `proxy.spec.ts`: Controlled pauses at the greeting, SSL response, and TLS handshake check cleanup before negotiation completes. Delayed SSL responses also check that buffering preserves startup bytes for TLS and fallback paths. Co-authored-by: Henry Jang <jang@hyochan.site>
Failed startup already tears down its development environment, but a teardown error can replace the failure the caller needs to diagnose. Preserve the original error both when startup throws and when it fails before becoming ready, even if cleanup also rejects. For a started session, repeated dispose() calls share one teardown promise, including its rejection. Cleanup is attempted once rather than starting overlapping teardown operations. Co-authored-by: Henry Jang <jang@hyochan.site>
A database greeting or a separately fetched connection string does not prove that the local binding can authenticate through its bridge. Exercise actual SQL queries against provisioned MySQL and PostgreSQL Hyperdrive resources using ordinary database drivers. The drivers and PostgreSQL types are test-only dependencies, not additions to the runtime bundle. - `hyperdrive-remote.test.ts`: Each protocol runs concurrent queries through wrangler dev and a Node-side query through getPlatformProxy(). An invalid local connection string confirms remote mode does not use the local fallback. - `miniflare-remote-resources.test.ts`: A direct Miniflare consumer configures the remote binding and runs a MySQL query before and after setOptions(). This exercises the shared implementation independently of Wrangler's dev adapter. Co-authored-by: Henry Jang <jang@hyochan.site>
8fd130d to
3c9053f
Compare
edmundhung
left a comment
There was a problem hiding this comment.
Thanks for your patience, @mack-erel and sorry that it took us so long before we can merge your contribution. I have brought the changes tested in #16070 back here.
The main differences are:
- Credentials and database connections now use the same remote session, avoiding authentication mismatches and removing the periodic keepalive workaround.
- Reworked bridge ownership and cleanup so reloads preserve existing connections, while shutdown closes everything.
- Simplified the remote configuration and kept the shared behavior in Miniflare, with thin Wrangler/Vite integration.
workers-devprod
left a comment
There was a problem hiding this comment.
Codeowners reviews satisfied

Fixes #14842.
What
hyperdrivebindings arelocal-onlytoday:wrangler devconnects straight tolocalConnectionString, so a database that is only reachable through the deployed Hyperdrive configuration — for example one whose origin is a private-network VPC Service — cannot be exercised locally at all.This adds
remote: trueforhyperdrivebindings, matching the existing opt-in onkv_namespace/r2_bucket/d1:{ "hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<your-hyperdrive-id>", "remote": true }] }With it set, local dev talks to the deployed Hyperdrive configuration and standard drivers (
mysql2,pg) work unchanged.How
A local TCP bridge, not a Worker designator. Miniflare's
HyperdriveProxyControllergainscreateRemoteTcpBridge, which listens on127.0.0.1and relays each connection byte-for-byte to the edge through the remote-bindings proxy's existingconnecthandler (#14712). Thehyperdrive:<name>designator stays anexternal.tcpservice pointing at that bridge, so workerd is unmodified.That last part is deliberate rather than incidental: pointing a Hyperdrive designator at a Worker service segfaults workerd (cloudflare/workerd#6901). I built workerd from source to check whether that was a quick fix — even with the client-side lifetime fix applied (attaching the adapter and
WorkerInterfaceto the returned stream, mirroringconnectImpl), the crash goes away but bytes still don't flow through a Worker target due to a second, target-side teardown issue. So the relay runs in the Miniflare Node process instead.Credential seeding. The edge mints credentials for the connection it opens rather than for the binding in the abstract, so
ProxyServerWorkerhands them back on the WebSocket upgrade response (MF-HD-Connection-String) of a connection it has just opened.seedRemoteHyperdriveBindingsreads them from there and returns them; the caller passes them into the (synchronous)buildMiniflareBindingOptionsrather than writing them onto the binding objects, which are shared by reference with the record the remote proxy session keeps for change detection. That is what lets drivers authenticate through the edge proxy, and whylocalConnectionStringbecomes optional for these bindings.Seeding retries before giving up, and a failure that outlasts the retries degrades rather than aborting: the Hyperdrive bindings warn and fall back, while the session stays usable for every other remote binding.
Fully opt-in — without
remote: true, the existing local-only path is unchanged.Testing
Tests in
remote-bindings-connect.spec.tsdrive a raw TCP client throughcreateRemoteTcpBridgeinto a real (bundled)ProxyServerWorker, asserting bidirectional byte relay and that the upgrade response carries the edge binding's connection string.seed-hyperdrive-bindings.test.tscovers the retry and give-up paths;maybe-start-or-update-session.test.tscovers session reuse, the credential keepalive (including retargeting it when bindings change), and the degrade-on-failure path.Current local run:
@cloudflare/remote-bindings30/30, Miniflare's Hyperdrive and remote-bindings suites 49/49 (remote-bindings-connect.spec.tsis 16 of those), thepackages/wranglerHyperdrive tests 81/81, andcheck:typeacross all 203 workspace tasks. oxfmt and oxlint are clean repo-wide.Also verified end-to-end against a real Hyperdrive configuration (MySQL behind a Cloudflare Tunnel): a stock
mysql2pool authenticates andSELECT VERSION()returns the origin server's version rather than the proxy banner — i.e. queries reach the real database, as in production.Known constraints
MF-Connect-Addressis sent as a fixedhyperdrive.local:0— Hyperdrive's edgeconnect()ignores the address, but the relay path requires the header to be present.On the opt-in semantics
#14842 raised three possible shapes for the opt-in, and this PR implements the simplest one (
remote: true, consistent with the other bindings). The other two are implemented on the same core and I am happy to swap either in: a fallback variant (fall back tolocalConnectionStringwhen the remote session cannot be established), and an experimental-flag-gated variant. I opened this one as a concrete starting point rather than leaving the question abstract — say the word and I will re-submit in whichever direction you prefer.remotecarries the same meaning here as the already-documented opt-in on KV, R2 and D1 bindings. Happy to open a Cloudflare Docs PR for the Hyperdrive binding page if you'd like it called out there.