Skip to content

[miniflare] Support remote Hyperdrive bindings in local dev - #14900

Merged
edmundhung merged 4 commits into
cloudflare:mainfrom
henryj-dev:hyperdrive-remote
Oct 20, 2026
Merged

edmundhung merged 4 commits into
cloudflare:mainfrom
henryj-dev:hyperdrive-remote

Conversation

@mack-erel

@mack-erel mack-erel commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #14842.

What

hyperdrive bindings are local-only today: wrangler dev connects straight to localConnectionString, 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: true for hyperdrive bindings, matching the existing opt-in on kv_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 HyperdriveProxyController gains createRemoteTcpBridge, which listens on 127.0.0.1 and relays each connection byte-for-byte to the edge through the remote-bindings proxy's existing connect handler (#14712). The hyperdrive:<name> designator stays an external.tcp service 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 WorkerInterface to the returned stream, mirroring connectImpl), 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 ProxyServerWorker hands them back on the WebSocket upgrade response (MF-HD-Connection-String) of a connection it has just opened. seedRemoteHyperdriveBindings reads them from there and returns them; the caller passes them into the (synchronous) buildMiniflareBindingOptions rather 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 why localConnectionString becomes 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.ts drive a raw TCP client through createRemoteTcpBridge into a real (bundled) ProxyServerWorker, asserting bidirectional byte relay and that the upgrade response carries the edge binding's connection string. seed-hyperdrive-bindings.test.ts covers the retry and give-up paths; maybe-start-or-update-session.test.ts covers session reuse, the credential keepalive (including retargeting it when bindings change), and the degrade-on-failure path.

Current local run: @cloudflare/remote-bindings 30/30, Miniflare's Hyperdrive and remote-bindings suites 49/49 (remote-bindings-connect.spec.ts is 16 of those), the packages/wrangler Hyperdrive tests 81/81, and check:type across 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 mysql2 pool authenticates and SELECT VERSION() returns the origin server's version rather than the proxy banner — i.e. queries reach the real database, as in production.

Known constraints

  • Seeding runs at session start and again on every reload, because the credentials have to be read off a live connection; they are not cached for the session's lifetime.
  • Credentials come from a throwaway connection, and a later database connection is a different one. If the edge does not keep a session's connections on a single instance, the two can disagree — see the discussion on [miniflare] Verify remote Hyperdrive bindings in local dev (#14900) #16070.
  • The bridge inherits the raw-TCP relay's limits from [miniflare] Support connect() on remote VPC Network bindings in local dev #14712: no TCP half-close propagation, and no backpressure plumbing.
  • MF-Connect-Address is sent as a fixed hyperdrive.local:0 — Hyperdrive's edge connect() 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 to localConnectionString when 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.


  • Tests
    • Tests included/updated
  • Public documentation
    • Documentation not necessary because: remote carries 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.

A raccoon waving hello


Open in Devin Review

@changeset-bot

changeset-bot Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3c9053f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 15 packages
Name Type
@cloudflare/config Minor
miniflare Minor
wrangler Minor
@cloudflare/vite-plugin Minor
@cloudflare/workers-utils Minor
@cloudflare/autoconfig Patch
@cloudflare/build-output-utils Patch
@cloudflare/containers-shared Patch
@cloudflare/deploy-helpers Patch
@cloudflare/vitest-plugin Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/cli-shared-helpers Patch
@cloudflare/workers-auth Patch

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

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Jul 29, 2026
@workers-devprod
workers-devprod requested review from a team and edmundhung and removed request for a team July 29, 2026 00:20
@workers-devprod

workers-devprod commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/hyperdrive-remote-bindings-local-dev.md: [@cloudflare/wrangler]
  • packages/config/src/bindings.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/hyperdrive-proxy.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/shared/remote-bindings-connect.spec.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/index.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/seed-hyperdrive-bindings.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/templates/remoteBindings/ProxyServerWorker.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/binding-local-support.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/environment.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/validation.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/worker.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/dev/miniflare-hyperdrive.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/miniflare/index.ts: [@cloudflare/wrangler]

@pkg-pr-new

pkg-pr-new Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@14900

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@14900

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@14900

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@14900

@cloudflare/containers-shared

npm i https://pkg.pr.new/@cloudflare/containers-shared@14900

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@14900

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@14900

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@14900

miniflare

npm i https://pkg.pr.new/miniflare@14900

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@14900

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@14900

@cloudflare/runtime-types

npm i https://pkg.pr.new/@cloudflare/runtime-types@14900

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@14900

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@14900

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@14900

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@14900

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@14900

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@14900

wrangler

npm i https://pkg.pr.new/wrangler@14900

commit: 3c9053f

devin-ai-integration[bot]

This comment was marked as resolved.

@mack-erel
mack-erel force-pushed the hyperdrive-remote branch from 147ec12 to 429ba95 Compare July 29, 2026 00:45
@mack-erel

Copy link
Copy Markdown
Contributor Author

Thanks — both are fair. Fixed the first; here's my reasoning on the second.

Empty connection string / cryptic error (dev.ts:491) — fixed. hyperdriveEntry now throws a UserError when a binding has remote: true but neither a remote proxy session nor a localConnectionString, instead of passing "" down to Miniflare's URL validation. The message points at both remedies (authenticate for remote mode, or set localConnectionString / the env var).

I also covered the adjacent case Devin didn't flag: when the session is unavailable but a localConnectionString exists, the binding still falls back to it — that's the right behaviour for offline dev, so I kept it, but it now emits a logger.once.warn so a silent remote→local downgrade doesn't turn into a "why is my data different" debugging session. Four unit tests in packages/wrangler/src/__tests__/dev/miniflare-hyperdrive.test.ts cover local / remote-with-session / remote-with-neither / remote-with-fallback.

MF-HD-Seed guarded only by a header — worth stating explicitly, but I think it's the correct boundary rather than a gap. The endpoint sits behind exactly the same gate as the existing fetch and JSRPC relays on this worker: reaching it requires the remote proxy session URL, and the binding name is validated the same way. More importantly it grants no capability beyond what the session already confers — anyone who can reach the session can already open a tunnelled connection to the same database through the connect() relay, so handing back the connection string doesn't widen the blast radius. The seeded value is never logged on either side.

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 @cloudflare/remote-bindings, which is a private package, and the PR description was missing the template checklist.

@mack-erel

Copy link
Copy Markdown
Contributor Author

@xortive this came out of your note on #14712 that the same relay would probably work for Hyperdrive remote: true — it does, with one caveat.

Since you're on Hyperdrive, you'd be the right person to sanity-check two things:

  1. The opt-in shape. I went with plain remote: true on the binding, consistent with KV/R2/D1 and with [miniflare] Support connect() on remote VPC Network bindings in local dev #14712 (no new user-facing flag). 🚀 Feature: support remote Hyperdrive bindings in local dev (raw TCP relay) #14842 lists two alternatives I've also implemented — a localConnectionString fallback variant, and an experimental-flag-gated one — happy to swap either in.

  2. The credential seeding. The edge mints per-session credentials, so the local binding has to present the edge session's connectionString or drivers fail auth right after the greeting. I fetch it once per session through a guarded MF-HD-Seed endpoint on the proxy worker. That's the one genuinely new seam here, and the piece most worth a Hyperdrive-side opinion.

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 external.tcp and runs a small local TCP bridge that relays to the edge instead. Your vpc-services note from #14712 is in as well, verified against a real tcp-type VPC Service.

CI is green, and a stock mysql2 pool authenticates end-to-end against a real Hyperdrive config — SELECT VERSION() returns the origin server's version rather than the proxy banner, so queries reach the actual database. No rush on this; just flagging it since it started as your idea.

@workers-devprod

workers-devprod commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/hyperdrive-remote-bindings-local-dev.md: [@cloudflare/wrangler]
  • packages/config/src/bindings.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/hyperdrive-proxy.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/shared/remote-bindings-connect.spec.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/index.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/seed-hyperdrive-bindings.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/templates/remoteBindings/ProxyServerWorker.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/binding-local-support.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/environment.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/validation.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/worker.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/dev/miniflare-hyperdrive.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/integrations/platform/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/miniflare/index.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

@mack-erel

Copy link
Copy Markdown
Contributor Author

Gentle status ping — no urgency, just keeping this from going stale.

Since the last update this branch has been rebased onto main and picked up the review feedback:

  • Credential seeding now covers every async dev path. It previously ran only in LocalRuntimeController, so getPlatformProxy() and multi-worker dev built the binding with placeholder credentials and failed to authenticate — silently. Seeding moved into buildMiniflareOptions / getMiniflareOptionsFromConfig, and the one remaining synchronous entry point (unstable_getMiniflareWorkerOptions, used by the Vite plugin and vitest-pool-workers) now warns instead of failing quietly.
  • Seeding no longer mutates shared binding objects, which was making the remote proxy session tear down and reconnect on every hot reload.
  • The remote TCP bridge closes its previous listener before re-registering, instead of leaking one per reload.

On the CI red: the four dev-registry.spec.ts failures are not from this branch. They reproduce on a clean checkout of upstream/main with none of my commits applied, and were introduced by #15040. This PR touches neither dev-registry nor the Vite plugin. Everything this branch adds is green.

That leaves @cloudflare/wrangler codeowners approval as the only thing outstanding. Happy to rebase again or split this up if that would make it easier to review.

A kitten waiting patiently by a phone

@workers-devprod

workers-devprod commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/hyperdrive-remote-bindings-local-dev.md: [@cloudflare/wrangler]
  • packages/config/src/bindings.ts: [@cloudflare/wrangler]
  • packages/config/src/schema.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/hyperdrive-proxy.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/shared/remote-bindings-connect.spec.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/index.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/seed-hyperdrive-bindings.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/templates/remoteBindings/ProxyServerWorker.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/binding-local-support.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/environment.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/validation.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/worker.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/dev/miniflare-hyperdrive.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/integrations/platform/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/miniflare/index.ts: [@cloudflare/wrangler]

@mack-erel

Copy link
Copy Markdown
Contributor Author

Rebased onto main again — #14994 landed the new miniflare config structure in the meantime, which this branch had to be rewritten against rather than merely merged.

The rewrite is a simplification. The new structure already carries remote on env bindings and exposes getRemoteProxyConnectionString, so the custom HyperdriveEntrySchema and normalizeHyperdriveEntry this PR previously added are gone; the plugin now resolves bindings the same way KV, R2 and D1 do. HyperdriveBindingSchema is a strictObject, so remote also had to be declared there for it to survive parsing.

Also fixed while here: buildMiniflareOptions was loading api/remoteBindings unconditionally, including on local-only dev sessions where seedRemoteHyperdriveBindings returns immediately. That import is now guarded on remoteProxyConnectionString, matching how LocalRuntimeController already gates its own.

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 getEnvBindingsOfType + getRemoteProxyConnectionString is how you intended plugins to consume the new structure, and whether adding remote to HyperdriveBindingSchema is the right place for that opt-in. Happy to rework either. No urgency — I know the config migration is the bigger thing in flight right now.

On CI: one job is red, Vite Plugin Playground (windows-latest, vite-8), where the react-spa beforeAll hook exceeds its 50s timeout. I don't think it's this branch — the same job passed on the immediately preceding commit, the only change between the two is 11 added and 6 removed lines in packages/wrangler/src/dev/miniflare/index.ts, and the react-spa playground references neither Hyperdrive nor remote bindings. A re-run would settle it, but I can't trigger one. Everything else is green.

@mack-erel

Copy link
Copy Markdown
Contributor Author

Correcting my last comment: the red Vite Plugin Playground (windows-latest) job was this branch. I was wrong to wave it off.

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 (react-spa booted in 2.8s in the failing run) but the reload afterwards: that spec rewrites public/_redirects mid-test and waits for the config to reload, and buildMiniflareOptions sits on the reload path for every dev session.

The seeding step there was guarded only on remoteProxyConnectionString, so any session with a remote binding of any kind paid for a lazy module load plus an extra await on every reload — including configs with no Hyperdrive binding at all. It's now guarded on an actual remote Hyperdrive binding, so configs that don't use this feature take a synchronous check and nothing else (fa6cbe8).

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 buildMiniflareOptions, I'm happy to move it — it's on that path precisely because it needs to cover getPlatformProxy() and multi-worker dev too, but I don't have strong feelings about the placement.

Also rebased onto current main along the way.

@workers-devprod

workers-devprod commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/hyperdrive-remote-bindings-local-dev.md: [@cloudflare/wrangler]
  • packages/config/src/bindings.ts: [@cloudflare/wrangler]
  • packages/config/src/schema.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/config/schema.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/config/v4-convert.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/config/v4-schema.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/hyperdrive-proxy.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/shared/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/hyperdrive/index.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/shared/remote-bindings-connect.spec.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/index.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/maybe-start-or-update-session.test.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/maybe-start-or-update-session.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/seed-hyperdrive-bindings.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/templates/remoteBindings/ProxyServerWorker.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/miniflare-options.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/config.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/binding-local-support.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/environment.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/validation.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/worker.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/remote-binding/miniflare-remote-resources.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/e2e/remote-binding/workers/hyperdrive.js: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/dev/miniflare-hyperdrive.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/integrations/platform/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/MultiworkerRuntimeController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/miniflare/index.ts: [@cloudflare/wrangler]

@edmundhung edmundhung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/miniflare/test/plugins/shared/remote-bindings-connect.spec.ts Outdated
Comment thread packages/miniflare/src/plugins/hyperdrive/index.ts Outdated
Comment thread packages/miniflare/src/plugins/hyperdrive/index.ts Outdated
Comment thread packages/wrangler/src/dev/miniflare/index.ts Outdated
@mack-erel

Copy link
Copy Markdown
Contributor Author

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 localConnectionString alone and drops remoteProxyConnectionString, so the plugin saw no remote string and never created the bridge. Understandable — this PR wasn't merged when that was written, and the other remote-capable bindings go through the shared isRemote collector while this one didn't. Two related constraints came with it (the v4 schema only accepted a connection string; miniflare required localConnectionString on every Hyperdrive binding, which a remote one doesn't have). Fixed in 0fd040d. Our unit tests passed throughout that regression — they cover the pieces, not the wiring — which is exactly the gap the new E2E case closes.

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 packages-and-tools failures are vitest-plugin's new-config.test.ts timing out at 15s. That test uses neither remote bindings nor Hyperdrive, it fails the same way on upstream main, and it passes locally — this branch's only change in that package passes one extra field through. Everything else is green.

edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 19, 2026
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>
edmundhung added a commit that referenced this pull request Oct 20, 2026
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>
edmundhung added a commit that referenced this pull request Oct 20, 2026
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>
@workers-devprod

workers-devprod commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

@clanky-the-clanker

Copy link
Copy Markdown

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 clanky review (or clanker review or @clanky-the-clanker review), optionally followed by instructions (e.g. clanky review focus on the retry logic).

edmundhung and others added 4 commits October 20, 2026 14:44
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>

@edmundhung edmundhung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 workers-devprod left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codeowners reviews satisfied

@edmundhung
edmundhung merged commit 08f22d3 into cloudflare:main Oct 20, 2026
231 of 247 checks passed
@github-project-automation github-project-automation Bot moved this from Untriaged to Done in workers-sdk Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

🚀 Feature: support remote Hyperdrive bindings in local dev (raw TCP relay)

4 participants