Skip to content

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

Open
mack-erel wants to merge 19 commits into
cloudflare:mainfrom
mack-erel:hyperdrive-remote
Open

[miniflare] Support remote Hyperdrive bindings in local dev#14900
mack-erel wants to merge 19 commits into
cloudflare:mainfrom
mack-erel: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 per-session credentials, so a header-guarded MF-HD-Seed endpoint on ProxyServerWorker returns the edge binding's connectionString, and seedRemoteHyperdriveBindings writes it into the local binding config before the (synchronous) buildMiniflareBindingOptions runs. That is what lets drivers authenticate through the edge proxy, and why localConnectionString becomes 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.ts drive a raw TCP client through createRemoteTcpBridge into a real (bundled) ProxyServerWorker, asserting bidirectional byte relay, plus the MF-HD-Seed happy path, a 400 on a missing MF-Binding header, and a 404 on an unknown binding. Suite 19/19; packages/wrangler hyperdrive tests 65/65; turbo build check:type 34/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 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 once per dev session, since the edge credentials are per-session.
  • 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: 854f1f7

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

This PR includes changesets to release 8 packages
Name Type
miniflare Minor
wrangler Minor
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-plugin 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

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/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: 854f1f7

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.

// 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 ({

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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}`,

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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]) => {

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.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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()\`.`

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@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.

@workers-devprod

workers-devprod commented Aug 25, 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/deploy-helpers/src/deploy/helpers/print-bindings.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/config/schema.spec.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/vitest-plugin/test/remote-proxy-sessions.test.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.test.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/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]

@mack-erel

Copy link
Copy Markdown
Contributor Author

Rebased onto main, fixed the red miniflare job, and verified the feature against real Hyperdrive configurations rather than against CI alone. One new commit and one known issue, both below.

The rebase was a migration, not a replay. #15318 landed in the meantime and moved development-only binding configuration under a dev key — remote became dev.remote, and Hyperdrive's localConnectionString became dev.connectionString. That is exactly the surface this branch changes, so the five conflicting files had to be rewritten against the new shape.

It removed a wart this PR was carrying. This branch used to declare remote on the Hyperdrive binding itself, because HyperdriveBindingSchema is a strictObject that would otherwise drop it. There is now a shared shape for precisely that, so Hyperdrive composes it the way R2 does:

// packages/config/src/schema.ts
dev: RemoteBindingDevSchema.extend({
    connectionString: z.string().optional(),
}).optional(),

with BindingDevOptions & { connectionString?: string } as the matching type. Nothing Hyperdrive-specific describes remote any more — it inherits it, and resolves through the same getRemoteProxyConnectionString(binding, dev) as every other remote-capable binding. This is the direction I flagged last time about the shared isRemote collector; the field shape now matches as well. The remainder is mechanical: v4-convert assembles a dev object, the miniflare-side refinement keys off dev.remote / dev.connectionString, and the plugin reads binding.dev.connectionString.

The failing test was hiding a coverage gap. test/config/schema.spec.ts asserted zod's default "Invalid input: expected string, received undefined", which this PR replaces with a refinement explaining the remote exemption; main had already renamed the test and updated its issue path, so only the message needed to move. But that test only covered the two cases predating this PR — a missing connection string, and a valid local one. The case this PR actually introduces, a remote binding parsing without a connection string, was asserted nowhere. It is now.

Verified against real Hyperdrive configurations. A throwaway Worker with remote: true and no localConnectionString on two bindings, one Postgres and one MySQL, run under wrangler dev from this branch:

binding query result
Postgres SELECT version() PostgreSQL 17.10 on x86_64-pc-linux-musl …
MySQL SELECT VERSION() 8.0.25

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: workers/hyperdrive.js asserts the MySQL server greeting (protocolVersion: 10), and the greeting is sent before authentication, so that assertion passes even when the seeded per-session credentials are wrong. The E2E case also builds Miniflare options directly, bypassing the wrangler dev path (hyperdriveEntryv4-convert → plugin) where this feature has actually regressed before. It is still worth keeping — it covers the bridge and the relay — but a green E2E run is not on its own evidence that a remote binding authenticates. Running a real query is.

New commit: the dev binding table reported remote Hyperdrive bindings as local. printBindings hard-coded isSimulatedLocally: true for every Hyperdrive binding, so a remote: true binding printed local while it was demonstrably reaching the edge. It now reads remote off the binding like the other remote-capable bindings do. The regression test fails against the previous code (remotelocal), so it is load-bearing.

Known issue, tracked separately in #15345. Every remote Hyperdrive connection makes the runtime log The Workers runtime canceled this request because it detected that your Worker's code had hung. Queries are unaffected — the responses are correct and fast — but the log line is alarming and appears once per connection, for both the Postgres and the MySQL binding.

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 handleConnect relay itself: a VPC-network connect() binding driving that same relay logs none, under every variation I tried. #15345 has the reproduction, the measurements, and the four hypotheses I ruled out, since chasing it further would widen this diff into the shared relay.

@mack-erel

Copy link
Copy Markdown
Contributor Author

#15345 is fixed, in 8fc67128 on this branch — the known issue in my previous comment no longer applies.

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 Terminate, MySQL COM_QUIT), that message has already been relayed, and the edge closes the tunnel itself about 10ms later — so the bridge was racing it, and when the bridge won the runtime severed the edge relay mid-flight and reported it as a request that hung. The 1:1 correlation with close code 1006 I reported earlier was a symptom of the same severed relay, not the trigger.

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. check:type passes across all 204 tasks and the miniflare suite is green.

Worth noting for the review: the relay in ProxyServerWorker was not at fault. A VPC-network connect() binding driving that same relay never reproduced this, so the change is confined to hyperdrive-proxy.ts and does not touch shared code.

@mack-erel

Copy link
Copy Markdown
Contributor Author

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.

miniflare-remote-resources.test.ts gates the whole matrix:

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. CLOUDFLARE_ACCOUNT_ID is empty, the matrix is skipped in full, and I could not find should work for Hyperdrive in any of the four Linux shards. What does run is the "remote proxy session disabled" block — Browser and Pipelines. The hyperdrive dev tests in e2e/dev.test.ts are skipped too, since HYPERDRIVE_DATABASE_URL and HYPERDRIVE_MYSQL_DATABASE_URL are empty for the same reason.

So the green Wrangler E2E shards on this PR say nothing about Hyperdrive either way, and the only execution evidence for this feature is the manual run against real Postgres and MySQL configurations I described earlier. Whoever picks this up may want to run the matrix from a branch on this repository before merging.

On the two red checks, both look unrelated to this branch:

  • Wrangler E2E (Linux, shard 1/4)basic python dev > can modify entrypoint during wrangler dev fails with readUntil() timed out matching /Ready on .../, stuck at ⎔ Starting local server.... A Python worker that did not boot inside the timeout; no Hyperdrive binding is involved, so nothing this PR changes is on that path.
  • Vite Plugin Playground (ubuntu-latest, vite-8) — the beforeAll server hook in react-spa/__tests__/experimental-headers-and-redirects timed out at 50s. I did not wave this one off, because this branch genuinely broke that spec once before via the reload path. That regression was in the reload, this is the startup hook, it passed on the previous commit of this branch, and it passes locally here (6 passed, 3 expected fail). The change in 8fc67128 only executes per bridged Hyperdrive connection, which that playground has none of. A re-run would settle it, and I cannot trigger one from a fork.

mack-erel and others added 19 commits August 26, 2026 13:37
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

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

3 participants