Skip to content

fix: Keep debug port connection alive for WebSocket passthrough#6100

Merged
penalosa merged 3 commits intomainfrom
penalosa/debug-port-websocket-fix
Feb 18, 2026
Merged

fix: Keep debug port connection alive for WebSocket passthrough#6100
penalosa merged 3 commits intomainfrom
penalosa/debug-port-websocket-fix

Conversation

@penalosa
Copy link
Copy Markdown
Contributor

@penalosa penalosa commented Feb 18, 2026

Problem

When a WebSocket is obtained via the debug port and passed through a service binding response (from a WorkerEntrypoint.fetch()), the WebSocket becomes invalid because the debug port TCP connection is destroyed when the intermediate IoContext finishes.

This affects miniflare's dev-registry, which uses the debug port to proxy service bindings between workers running in different workerd instances.

Root Cause

DebugPortConnectionState was stored via context.addObject(), tying its lifetime to the IoContext. When the IoContext that called getEntrypoint() completed, the connection was destroyed, invalidating any WebSockets still using that connection.

Fix

Make DebugPortConnectionState refcounted (kj::Refcounted) and ensure that:

  1. WorkerdBootstrapSubrequestChannel holds a reference to the connection
  2. RpcWorkerInterface returned by startRequest() has the connection attached via .attach(kj::addRef(*connectionState))
  3. WorkerdDebugPortClient stores the state as kj::Own (not IoOwn)

This ensures the connection lives as long as any active WebSockets.

Testing

Added a test that:

  1. Creates a target worker that accepts WebSocket upgrades
  2. Creates a proxy worker with a WorkerEntrypoint that gets a WebSocket via debug port and passes it through
  3. Verifies that WebSocket messages work correctly through the passthrough

All existing tests continue to pass.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Feb 18, 2026

Merging this PR will not alter performance

✅ 70 untouched benchmarks
⏩ 129 skipped benchmarks1


Comparing penalosa/debug-port-websocket-fix (9fc45e9) with main (8c16dda)

Open in CodSpeed

Footnotes

  1. 129 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Copy Markdown
Collaborator

@danlapid danlapid left a comment

Choose a reason for hiding this comment

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

This is basically caused by deferred proxying optimizing your worker away.
Seems fine to support that.

When a WebSocket is obtained via the debug port and passed through a
service binding response (WorkerEntrypoint.fetch()), the WebSocket
would become invalid because the debug port TCP connection was being
destroyed when the intermediate IoContext finished.

This happened because DebugPortConnectionState was stored via
context.addObject(), tying its lifetime to the IoContext. When the
IoContext that called getEntrypoint() completed, the connection was
destroyed, invalidating any WebSockets still using it.

Fix: Make DebugPortConnectionState refcounted and ensure that:
1. WorkerdBootstrapSubrequestChannel holds a reference
2. RpcWorkerInterface returned by startRequest() has the connection
   attached via .attach(kj::addRef(*connectionState))
3. WorkerdDebugPortClient stores the state as kj::Own (not IoOwn)

This ensures the connection lives as long as any active WebSockets.
@penalosa penalosa force-pushed the penalosa/debug-port-websocket-fix branch from 74fe1cd to 754a2d4 Compare February 18, 2026 16:47
@penalosa penalosa enabled auto-merge (squash) February 18, 2026 23:05
@penalosa penalosa merged commit f3fdde1 into main Feb 18, 2026
22 checks passed
@penalosa penalosa deleted the penalosa/debug-port-websocket-fix branch February 18, 2026 23:06
penalosa added a commit to cloudflare/workers-sdk that referenced this pull request Feb 19, 2026
…port RPC

Remove the HTTP fallback, eager body buffering, _client GC hack, and WebSocket
special-casing from the dev-registry proxy workers. These workarounds existed
because the workerd debug port would close connections prematurely when the
initial subrequest completed. With the refcounted DebugPortConnectionState fix
in workerd (cloudflare/workerd#6100), connections now survive as long as any
response body or WebSocket is in use.

Key changes:
- Replace hasAssets/entryAddress with defaultEntrypointService and
  userWorkerService fields in WorkerDefinition/RegistryEntry
- Route DOs and named entrypoints through userWorkerService (bypassing
  assets/vite proxy layer) instead of hardcoding core:user: prefix
- Validate required registry fields in resolveTarget() to handle stale entries
- Inline and delete getWorkerdServiceName() helper
- Remove dead entryAddress field and fetchViaHttp code path
- Remove DO WebSocket 501 error and DO body buffering workarounds
penalosa added a commit to cloudflare/workers-sdk that referenced this pull request Feb 23, 2026
…port RPC

Remove the HTTP fallback, eager body buffering, _client GC hack, and WebSocket
special-casing from the dev-registry proxy workers. These workarounds existed
because the workerd debug port would close connections prematurely when the
initial subrequest completed. With the refcounted DebugPortConnectionState fix
in workerd (cloudflare/workerd#6100), connections now survive as long as any
response body or WebSocket is in use.

Key changes:
- Replace hasAssets/entryAddress with defaultEntrypointService and
  userWorkerService fields in WorkerDefinition/RegistryEntry
- Route DOs and named entrypoints through userWorkerService (bypassing
  assets/vite proxy layer) instead of hardcoding core:user: prefix
- Validate required registry fields in resolveTarget() to handle stale entries
- Inline and delete getWorkerdServiceName() helper
- Remove dead entryAddress field and fetchViaHttp code path
- Remove DO WebSocket 501 error and DO body buffering workarounds
penalosa added a commit to cloudflare/workers-sdk that referenced this pull request Mar 2, 2026
…port RPC

Remove the HTTP fallback, eager body buffering, _client GC hack, and WebSocket
special-casing from the dev-registry proxy workers. These workarounds existed
because the workerd debug port would close connections prematurely when the
initial subrequest completed. With the refcounted DebugPortConnectionState fix
in workerd (cloudflare/workerd#6100), connections now survive as long as any
response body or WebSocket is in use.

Key changes:
- Replace hasAssets/entryAddress with defaultEntrypointService and
  userWorkerService fields in WorkerDefinition/RegistryEntry
- Route DOs and named entrypoints through userWorkerService (bypassing
  assets/vite proxy layer) instead of hardcoding core:user: prefix
- Validate required registry fields in resolveTarget() to handle stale entries
- Inline and delete getWorkerdServiceName() helper
- Remove dead entryAddress field and fetchViaHttp code path
- Remove DO WebSocket 501 error and DO body buffering workarounds
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants