[miniflare] Support remote Hyperdrive bindings in local dev - #14900
[miniflare] Support remote Hyperdrive bindings in local dev#14900mack-erel wants to merge 19 commits into
Conversation
🦋 Changeset detectedLatest commit: 854f1f7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 8 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
create-cloudflare
@cloudflare/deploy-helpers
@cloudflare/kv-asset-handler
miniflare
@cloudflare/pages-functions
@cloudflare/pages-shared
@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.
| // binding via the same `handleConnect` path exercised above. A database client | ||
| // (mysql2/pg) then speaks its wire protocol straight through the bridge. | ||
| describe("Hyperdrive remote binding: local TCP bridge", () => { | ||
| test("relays bytes between a local TCP client and the edge binding", async ({ |
There was a problem hiding this comment.
Please add Hyperdrive to the existing remote-bindings E2E matrix in packages/wrangler/e2e/remote-binding/miniflare-remote-resources.test.ts. The helper already provisions real Hyperdrive resources, so having that case execute a real query would cover seeding, the bridge, and authentication together rather than testing them separately.
There was a problem hiding this comment.
Added in e35b2ec. The worker opens a raw connection through the binding and asserts on the MySQL handshake rather than running a query, which keeps the fixture free of a database driver dependency — the greeting comes from Hyperdrive's own proxy, which is the hop worth covering. I can't run it locally (it needs HYPERDRIVE_MYSQL_DATABASE_URL and an account), so CI is its first real execution.
There was a problem hiding this comment.
Correction to the above: this won't run on CI as things stand. check-remote-tests only hands out credentials for merge-queue runs, changeset-release/main, or PRs carrying the ci:run-remote-tests label — and I can't add a label. So the case is currently unexecuted anywhere; it needs that label (or the merge queue) before anyone can trust it.
| remoteProxyConnectionString, | ||
| }); | ||
| services.push({ | ||
| name: `${HYPERDRIVE_PLUGIN_NAME}:${name}`, |
There was a problem hiding this comment.
We should namespace this service and bridge by worker as well as binding. In a multi-worker setup, it's common for both workers to share the same binding name like DB.
There was a problem hiding this comment.
Done in 70a0efb. Services, proxies and bridges are now keyed by worker index as well as binding name, via getHyperdriveServiceName(workerIndex, name) — matching how the cache plugin and the core plugin's custom services namespace theirs. The MF-Binding header still carries the unqualified name, since that's what the edge dispatches on.
| getNodeBindings(options) { | ||
| return Object.fromEntries( | ||
| getHyperdrives(options.config).map(([name, url]) => { | ||
| getHyperdrives(options.config, options.dev).map(([name, url]) => { |
There was a problem hiding this comment.
Would this work with remote Hyperdrive?
const { env } = await getPlatformProxy();
const sql = postgres(env.HYPERDRIVE.connectionString);
await sql`SELECT 1`;The connection string currently contains a *.hyperdrive.local hostname that Node cannot resolve. Do you have thoughts on how we could make this usable from Node?
There was a problem hiding this comment.
It works now — 25926da6 and e0df6b2f. Two things were in the way.
The address: getNodeBindings had no way to reach the bridge port, since getServices creates it and the two don't share state. The proxy controller now records the port and getNodeBindings takes a context argument carrying the controller, so a remote binding reports 127.0.0.1:<port> instead of the unresolvable magic host.
Then authentication, which turned out to be a real bug: Hyperdrive issues credentials per connection and validates them on the socket it opened, but seeding fetched them through a separate MF-HD-Seed request. When that request and the later relay connection are served by different instances, the driver presents credentials that connection was never issued and the edge refuses with a bare "Internal error". The edge now reports the connection string on the connect upgrade response and seeding reads it from there. The relay still only moves bytes.
Verified against MySQL 8.0.25 and PostgreSQL 17.10, from both a Worker and getPlatformProxy(), with connectionString and with discrete fields.
| // below are synchronous — stay usable and fail visibly instead of | ||
| // silently. | ||
| logger.once.warn( | ||
| `The Hyperdrive binding "${hyperdrive.binding}" is configured with "remote": true, but its edge credentials could not be seeded in this context, so connections through it will likely fail to authenticate. Remote Hyperdrive bindings are currently supported in \`wrangler dev\` and \`getPlatformProxy()\`.` |
There was a problem hiding this comment.
We need to make sure the solution works with the Vite plugin and vitest-pool-workers too.
As both already await maybeStartOrUpdateRemoteProxySession(), could it also prepare and return the Hyperdrive connection strings? That would keep the session-specific setup in one place and ensure every consumer receives usable credentials.
There was a problem hiding this comment.
Moved into maybeStartOrUpdateRemoteProxySession() as you suggested — it returns the edge connection strings on RemoteProxySessionData. Since every consumer already awaits that call, wrangler dev, getPlatformProxy(), the Vite plugin and vitest-plugin all receive them without repeating the setup, and the work comes off the config-reload path that buildMiniflareOptions sits on.
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
|
Codeowners approval required for this PR:
Show detailed file reviewers
|
|
Rebased onto The rebase was a migration, not a replay. #15318 landed in the meantime and moved development-only binding configuration under a It removed a wart this PR was carrying. This branch used to declare // packages/config/src/schema.ts
dev: RemoteBindingDevSchema.extend({
connectionString: z.string().optional(),
}).optional(),with The failing test was hiding a coverage gap. Verified against real Hyperdrive configurations. A throwaway Worker with
Nothing was listening locally on 5432 or 3306 and no fallback warning appeared, so neither result can have come from a local fallback. That check turned out to matter more than I expected, and it is worth flagging for anyone reading the E2E case I added at your request: New commit: the dev binding table reported remote Hyperdrive bindings as Known issue, tracked separately in #15345. Every remote Hyperdrive connection makes the runtime log It correlates 1:1 with the bridge's WebSocket closing abnormally (1006) rather than cleanly (1000), and it does not originate in the user's Worker. The fault is not in the shared |
|
#15345 is fixed, in The bridge was closing its WebSocket as soon as the local client socket went away. A database client disconnects at the protocol level first (Postgres The bridge now lets the edge close the tunnel, and forces it down only if the edge does not — a client that dies without disconnecting cleanly. That forced path terminates rather than closing gracefully, since a close handshake needs the relay to run code it can no longer run and reproduces the report. Verified against real Postgres and MySQL Hyperdrive configurations: 30 connections across clean and abandoned disconnects, previously about one report per connection, now none, with query results unchanged. Worth noting for the review: the relay in |
|
Correcting something I said in my earlier comment: the Hyperdrive E2E case has never actually run in CI on this PR, so I was wrong to describe it as covering the bridge and the relay here.
if (!CLOUDFLARE_ACCOUNT_ID) {
describe.skip("Skipping remote bindings E2E tests because CLOUDFLARE_ACCOUNT_ID is not set");
} else {
describe("Remote bindings (remote proxy session enabled)", () => { /* the 19 cases */ });
}This PR comes from a fork, so GitHub does not expose repository secrets to its workflow runs. So the green On the two red checks, both look unrelated to this branch:
|
8fc6712 to
b092446
Compare
…idge Hyperdrive bindings could only ever point at a local database. Accept an object entry carrying a `remoteProxyConnectionString` alongside the existing plain connection string, which opts the binding into remote mode. For a remote binding, `HyperdriveProxyController.createRemoteTcpBridge` listens on 127.0.0.1 and relays each inbound connection byte-for-byte to the edge Hyperdrive binding over a WebSocket to the remote proxy's `connect` handler. The `hyperdrive:<name>` `external.tcp` designator points at that bridge, which keeps workerd unmodified — aiming a Hyperdrive designator at a Worker service crashes it. Local-only bindings keep taking the existing code path unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Allow `remote: true` on a `hyperdrive` binding to opt local dev into connecting to the deployed Hyperdrive configuration at the edge instead of a local database. Hyperdrive moves from `local-only` to `local-and-remote` in the binding support table, and the config validator accepts and validates the new field. When the flag is set and a remote proxy session is available, `hyperdriveEntry` hands miniflare the object form carrying the `remoteProxyConnectionString` so it can stand up the TCP bridge. `localConnectionString` is no longer required for these bindings, since there is no local database to point at. A session is not always there to be had — logged out, offline, or remote bindings turned off — so without one the binding falls back to its `localConnectionString` with a warning, and errors with instructions when it has none. Otherwise the empty connection string reaches miniflare and fails its URL validation with an opaque error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…entials The edge Hyperdrive proxy mints per-session credentials and uses the config id as the database name, so a database client reaching it through the TCP bridge must present those values — the user's local placeholder credentials only get as far as the server greeting. Add an `MF-HD-Seed` endpoint to ProxyServerWorker that returns a binding's edge `connectionString`. It is guarded on the header so it never intercepts ordinary connect/RPC/fetch traffic, and returns 400 without `MF-Binding` and 404 for a binding that exposes no connection string. `seedRemoteHyperdriveBindings` fetches that value for every remote Hyperdrive binding and writes it into the local binding config. `LocalRuntimeController` awaits it once the remote proxy session is ready and before miniflare options are built, since that step is synchronous. It is a no-op without a proxy session or remote Hyperdrive bindings. The seeded value is a live credential, so it is never logged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extend the remote bindings connect suite with the Hyperdrive path: a test that drives a raw TCP client through `createRemoteTcpBridge` into a real ProxyServerWorker and asserts bytes are relayed in both directions, and three covering the `MF-HD-Seed` endpoint — the happy path returning the binding's connection string, a 400 for a missing `MF-Binding` header, and a 404 for an unknown binding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ing bridges Addresses three issues raised in review: - Seeding ran only in `LocalRuntimeController`, so `getPlatformProxy()`, the Vite plugin and multi-worker dev built the binding with placeholder credentials (and a `mysql` scheme even for Postgres) and failed to authenticate at the edge, silently. Seeding now happens inside `buildMiniflareOptions` and `getMiniflareOptionsFromConfig`, covering every async dev path; `buildMiniflareBindingOptions` takes the seeded values as an argument and warns when they are absent, so the remaining synchronous entry point fails visibly rather than silently. - `seedRemoteHyperdriveBindings` mutated binding objects that are shared by reference with the record the remote proxy session keeps for change detection, so every hot reload compared unequal and tore down the session. It now returns the seeded values instead of mutating. - The remote TCP bridge was re-registered under the same key on each reload without closing the previous listener, leaking it and its live edge relays for the session's lifetime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
The seeding step moved into the shared async paths, so the changeset now states which entry points seed edge credentials and what happens under the synchronous `unstable_getMiniflareWorkerOptions()` used by the Vite plugin and vitest-pool-workers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
Two leftovers from rebasing onto the new miniflare config structure (cloudflare#14994): - `HyperdriveBindingSchema` is a `strictObject`, so `remote: true` was rejected at parse time even though the TypeScript interface allowed it. - The bridge and seed-endpoint tests still built Miniflare with the v4 top-level worker shape (`name`/`modules`/`hyperdrives`) instead of the `workers: [{ config: ... }]` form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
`buildMiniflareOptions` loaded `api/remoteBindings` on every call, including local-only `wrangler dev` sessions that have no remote proxy connection string. `seedRemoteHyperdriveBindings` returns immediately in that case, so the module load was pure overhead on a path that reloads on every config change. Guard it on `remoteProxyConnectionString` instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
…used `buildMiniflareOptions` runs on every config reload, and the seeding step guarded only on `remoteProxyConnectionString` — so any dev session with a remote binding of any kind paid for a lazy module load and an extra await per reload, including configs with no Hyperdrive binding at all. Guard on an actual remote Hyperdrive binding instead. Configs that do not use this feature now take a synchronous check and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
…ssion Per review: `maybeStartOrUpdateRemoteProxySession()` is already awaited by every consumer of a remote session, so it is a better home for the seeding step than `buildMiniflareOptions`. It now fetches each remote Hyperdrive binding's edge connection string once per session and returns them on `RemoteProxySessionData`. `wrangler dev` (single- and multi-worker) and `getPlatformProxy()` pass them straight through to the binding builder, and the Vite plugin and vitest-pool-workers — which already await the same call — can pick them up the same way. This also takes the work off the config-reload path: seeding used to run inside `buildMiniflareOptions`, which every reload goes through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
…y point Addresses two review comments. Services, proxies and TCP bridges were keyed on the binding name alone, so in a multi-worker setup two workers binding Hyperdrive under the same name (`DB` is the obvious case) collided on one service and one bridge. They are now keyed by worker index as well, matching how the cache plugin and the core plugin's custom services namespace theirs. Edge credentials also now reach the entry points that could not fetch them themselves: `unstable_getMiniflareWorkerOptions()` accepts them as an option, and the Vite plugin and vitest-pool-workers — which already await `maybeStartOrUpdateRemoteProxySession()` — pass through what the session prepared. The warning for a binding left without credentials no longer claims those environments are unsupported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
…l dev Verified against a real deployed Hyperdrive configuration: a Worker running under `wrangler dev` now queries the origin database through the bridge and gets the origin's own version back (MySQL 8.0.25), not the edge proxy banner. Both `env.HYPERDRIVE.connectionString` and the discrete host/user/password fields work. Four defects stood between the plumbing and a working connection: - `getBindings` took `workerIndex` in the `sharedOptions` position after the plugin interface gained an argument upstream, so miniflare failed to build. - The v4 conversion layer dropped the `remote` flag entirely and its schema only accepted a connection string, so the object form carrying `remoteProxyConnectionString` was rejected during config validation. It now registers the connection string through the shared `isRemote` collector, as the other remote-capable bindings do — that is what populates `dev.remoteProxyConnectionString`, which the plugin reads back. Without it the plugin saw no remote string and never stood up the bridge. - Miniflare required `localConnectionString` on every Hyperdrive binding. A remote binding has no local database to point at and is seeded from the edge session instead, so it is now optional for those and still required (with a clear message) for local ones. - Namespacing the bridge by worker also changed the `MF-Binding` header the edge relay dispatches on. The bridge key and the binding name are now passed separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
…atrix Adds Hyperdrive to the matrix, as requested in review. The helper already provisions a real Hyperdrive configuration, so the case exercises seeding, the local bridge and edge authentication together rather than separately. The worker opens a raw connection through the binding and asserts on the MySQL handshake instead of running a query, which keeps the fixture free of a database driver dependency. The greeting comes from Hyperdrive's own proxy rather than the origin — that is the hop this case is here to cover. Also replaces two hand-rolled copies of the remote proxy session data shape with `RemoteProxySessionData` itself; they had drifted from the real type once it gained the seeded connection strings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
A remote binding's connection string points at `<hex>.hyperdrive.local`, a host only workerd resolves via the binding's designator. Node has no such resolver, so `getPlatformProxy()` handed back an address that fails with ENOTFOUND — the credentials were there but unusable. The local TCP bridge is the way in, but its port lives in the proxy controller and `getNodeBindings` had no way to reach it. The controller now records the port per binding, and `getNodeBindings` takes a context argument carrying the controller, so a remote binding reports `127.0.0.1:<port>` with the same seeded credentials. Local bindings are untouched, and the other plugins ignore the new argument. Verified against a real deployed configuration: Node reaches the MySQL handshake through the bridge, and a raw round-trip returns the same bytes as the Worker designator path (95-byte greeting, 75-byte reply). Driver authentication from Node is still refused by the edge, which is the open question raised in review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
…ong to Hyperdrive issues credentials per connection and validates them on the socket it opened. Seeding fetched them through a separate `MF-HD-Seed` request, which can be served by a different instance than the one that later opens the relay — so a driver would present credentials that connection had never been issued, and the edge refused the login with a bare "Internal error". The edge now reports the connection string on the `connect` upgrade response, and seeding reads it from there by opening a throwaway relay connection. Same request, same connection, so the values always match. The relay still only moves bytes; nothing parses the wire protocol. On the Node side, every credential field is overridden alongside the address. `ProxyNodeBinding` wraps workerd's binding, so leaving `user`/`password`/ `database` un-overridden mixed one connection's address with another's credentials. Verified against two real deployed configurations — MySQL 8.0.25 and PostgreSQL 17.10 — across all four paths: `wrangler dev` and `getPlatformProxy()`, each with `connectionString` and with discrete fields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GDyTfEa62t3bSe5wWn8Mt
…table `printBindings` hard-coded `isSimulatedLocally: true` for every Hyperdrive binding, so a `remote: true` binding was reported as `local` even while it was reaching the edge. Read `remote` off the binding like the other remote-capable bindings do. Verified against real Hyperdrive configs: before the change both a remote and a local binding printed `local`; after it, only the local one does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every connection through a remote Hyperdrive binding logged "your Worker's code had hung". A database client disconnects at the protocol level (Postgres `Terminate`, MySQL `COM_QUIT`) before dropping its TCP connection, and that message has already been relayed, so the edge closes the tunnel itself within about 10ms. The bridge was closing the WebSocket as soon as its client socket went away, racing that: when the bridge won, the runtime severed the edge relay mid-flight and reported it as a request that hung. Wait for the edge to close instead, and force the tunnel down only if it does not — which is what happens when a client dies without disconnecting cleanly. That forced path terminates the socket rather than closing it gracefully, since a close handshake needs the relay to run code it can no longer run, which produces the same report. Measured against real Hyperdrive configurations: 30 connections across clean and abandoned disconnects, previously ~1 report per connection, now none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
b092446 to
854f1f7
Compare

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 per-session credentials, so a header-guarded
MF-HD-Seedendpoint onProxyServerWorkerreturns the edge binding'sconnectionString, andseedRemoteHyperdriveBindingswrites it into the local binding config before the (synchronous)buildMiniflareBindingOptionsruns. That is what lets drivers authenticate through the edge proxy, and whylocalConnectionStringbecomes optional for these bindings.Fully opt-in — without
remote: true, the existing local-only path is unchanged.Testing
Four new tests in
remote-bindings-connect.spec.tsdrive a raw TCP client throughcreateRemoteTcpBridgeinto a real (bundled)ProxyServerWorker, asserting bidirectional byte relay, plus theMF-HD-Seedhappy path, a 400 on a missingMF-Bindingheader, and a 404 on an unknown binding. Suite 19/19;packages/wranglerhyperdrive tests 65/65;turbo build check:type34/34 across miniflare + wrangler + remote-bindings; oxfmt and oxlint clean.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.