Skip to content

Conversation

@ghostwriternr
Copy link
Member

@ghostwriternr ghostwriternr commented Feb 11, 2026

Summary

This is a repro for the WebSocket close propagation bug discussed in capnproto/capnproto#2556. Sandbox SDK recently introduced a new API to connect xterm.js to a PTY within the container via Websocket. The SDK proxies a WebSocket from the container (via workerd’s tcpPort.fetch() upgrade path) and then returns that WebSocket to the browser. When testing this feature locally, messages proxy correctly in both directions, but closing the WebSocket does not work. Upon inspection, on the container side it looks like the close handshake never completes unless the connection is forcibly terminated. I wasn’t able to reproduce this behaviour in production; it seems specific to the local-dev stack.

The capnproto PR isolated the root cause to KJ's optimised WebSocket pumping, but broadly disabling optimised pumps is obviously not the right approach. This PR provides a workerd-level repro so the behaviour can be fixed here.

Details

The issue is specific to the eyeball coupling path. A worker that accepts the tcpPort.fetch() WebSocket internally via ws.accept() observes close events fine. The failure happens when that WebSocket is returned as Response.webSocket and workerd couples it to the external client.

Test design

Uses sh_test with workerd serve and an external Node WebSocket client rather than a stub-based wd_test. Stub-based tests use an in-runtime client that doesn't exercise the Response.webSocket coupling boundary, so it works well.

The test verifies a bidirectional echo (proving the connection works), then initiates a clean close and expects code 1000 back. It is expected to fail until the underlying bug is fixed — currently with Timed out after 5000ms waiting for close event.

Exercises the full eyeball path: external client connects over a real
TCP socket to workerd serve, which forwards through a Durable Object
to a container via getTcpPort(8080).fetch() with WebSocket upgrade.
Data proxies correctly; the close handshake does not complete.
@ghostwriternr ghostwriternr requested review from a team as code owners February 11, 2026 16:18
@github-actions
Copy link

github-actions bot commented Feb 11, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@ghostwriternr
Copy link
Member Author

I have read the CLA Document and I hereby sign the CLA

github-actions bot added a commit that referenced this pull request Feb 11, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 11, 2026

Merging this PR will degrade performance by 11.48%

❌ 1 regressed benchmark
✅ 69 untouched benchmarks
⏩ 129 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
Encode_ASCII_256[TextEncoder][0/0/256] 3.1 ms 3.5 ms -11.48%

Comparing ghostwriternr:repro/ws-close-eyeball (ed9df8d) with main (bca5351)

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.

ws.close(1000, 'client closing');
});

ws.addEventListener('close', (event) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You always need to make sure to reciprocate the close:

      ws.close(1000, 'closing in response to client close');

That is likely the bug you have

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah this is a gap in my repro, but even with reciprocation, I've verified the close event doesn't propagate back to the client. And my earliest test for this on the sandbox-sdk was a server-initiated close scenario, which would still remain.

I can try to expand on the repro to add these cases if that'd be useful!

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.

2 participants