Skip to content

[miniflare] Handle client socket errors in the Hyperdrive proxy - #15769

Open
Pduhard wants to merge 6 commits into
cloudflare:mainfrom
Pduhard:fix/hyperdrive-proxy-client-socket-errors
Open

Pduhard wants to merge 6 commits into
cloudflare:mainfrom
Pduhard:fix/hyperdrive-proxy-client-socket-errors

Conversation

@Pduhard

@Pduhard Pduhard commented Sep 22, 2026 •

Copy link
Copy Markdown

Fixes #15768.

The Hyperdrive proxy pipes the client socket and the database socket together. The database side has an error listener, the client side has none, so a client socket error has nothing listening and takes the Node process down. There are two windows where that happens.

After the pipes are set up. pipe() does not cover this. It attaches an onerror handler to the destination, but that handler removes itself and re-emits once no other listener is left. The four places that pipe the two sockets now go through one helper, pipeSockets, which attaches a teardown handler to both ends first.

Before they exist. On the TLS paths, #handleConnection writes to the database, reads the reply and runs a TLS handshake before it reaches a pipe site, and the client socket has no listener for that whole window — the case raised in review on the first revision. A listener is now attached as soon as the connection is accepted. Reaching this one takes a client that resets mid-negotiation: a Worker that half-closes, aborts its writer or throws leaves the proxy intact, and so does disposing of Miniflare mid-negotiation.

The two are complementary, not alternatives. The entry listener only has the dbSocket created at the top of #handleConnection in scope, and on the TLS paths the socket that ends up piped is a later newDbSocket or tlsSocket, so the pipe sites are where the right peer can be torn down.

One more teardown gap, also raised in review: on the fallback paths the database socket is opened after the negotiation, so it can be created once the client has already errored. Its error and close have fired by then, a listener attached at the pipe would never run, and pipe() only ends the destination for a source that ended cleanly — so the connection to the database stayed open with nothing to serve. pipeSockets now destroys the peer straight away when the client is already gone. There is no test for this one: the window is a client error landing inside the TCP connect of the replacement socket, and I could not make that deterministic.

Two things fell out of the helper. setupTLSConnection had become a pass-through to pipeSockets, so it is gone. createPlainTCPConnection no longer installs a handler of its own — its callers pipe the socket it returns on the next line — and its now-unused clientSocket parameter went with it.

packages/miniflare/test/plugins/hyperdrive/proxy.spec.ts covers both windows: a mock Postgres that upgrades to TLS and then writes continuously while the client is killed mid-stream, and a mock Postgres that sits on the SSL negotiation while the client resets. Both fail on main and pass here.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: internal socket lifecycle change, no public API surface touched.

Note

This is a contribution from an AI agent: Claude Code, Claude Opus 5. A human reviewed the diff and ran the reproduction, but wrote neither the patch nor this description.


Devin Review

Pduhard and others added 2 commits September 22, 2026 12:03
The local Hyperdrive proxy pipes the client socket and the database socket
together. Only the database side had an `error` listener, so an error on the
client socket had no listener and was thrown as an uncaught exception, taking
down the whole Node process.

Both sockets now go through a single helper that pipes them together and tears
down each side when either one errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e398757

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

This PR includes changesets to release 8 packages
Name Type
miniflare Patch
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-plugin Patch
wrangler 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 Sep 22, 2026
@workers-devprod
workers-devprod requested review from a team and NuroDev and removed request for a team September 22, 2026 10:05
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/hyperdrive-proxy-client-socket-errors.md: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/hyperdrive-proxy.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/hyperdrive/proxy.spec.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
@cloudflare/autoconfig

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

@cloudflare/build-output-utils

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

@cloudflare/codemods

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

@cloudflare/config

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

@cloudflare/containers-shared

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

create-cloudflare

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

@cloudflare/deploy-helpers

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

@cloudflare/kv-asset-handler

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

miniflare

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

@cloudflare/pages-functions

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

@cloudflare/pages-shared

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

@cloudflare/unenv-preset

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

@cloudflare/vite-plugin

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

@cloudflare/vitest-plugin

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

@cloudflare/workers-auth

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

@cloudflare/workers-editor-shared

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

@cloudflare/workers-utils

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

wrangler

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

commit: e398757

On the TLS paths, #handleConnection writes to the database, reads the reply
and runs a TLS handshake before it reaches a pipe site, and the client socket
has no error listener for that whole window. A client that resets
mid-negotiation still took the Node process down.

The listener is now attached as soon as the connection is accepted, alongside
the per-pipe teardown that knows which peer to destroy.

setupTLSConnection had become a pass-through to pipeSockets, and
createPlainTCPConnection no longer needs a handler of its own now that its
callers pipe the socket it returns on the next line; both are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Pduhard and others added 3 commits September 22, 2026 13:28
On the fallback paths the database socket is opened after the negotiation, so
it can be created when the client has already errored. Its `error` and `close`
have fired by then, a listener attached at the pipe would never run, and
`pipe()` only ends the destination for a source that ended cleanly — leaving
the connection to the database open with nothing to serve.

No test: the window is a client error landing inside the TCP connect of the
replacement socket, which I could not make deterministic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pipeSockets had the longest doc comment in the file on its smallest function,
most of it restating the two pipes and the two handlers below it. Only the
pipe() re-emit is worth writing down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The entry described the internals on both sides of the fix, so it never said
what a user would have seen or whether they were affected. It now names the
symptom, the surfaces it killed, and the mode that skips the proxy. It also
covers the fallback connection left open, which the entry did not mention.

Co-Authored-By: Claude Opus 5 <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.

[miniflare] Hyperdrive proxy crashes the Node process when the client socket errors

2 participants