cloudflare / cloudflare/workerd

WebSocket API (WebSocketPair) Disconnect Logic Broken

Open
#5,290 5 comments 1 reaction 0 assignees View on GitHub
bug
Dominant language
C++
Stars
8.7k
Forks
739
Avg merge
2d 20h
Merged PRs (30d)
174

Description

### What versions & operating system are you using?

MacOS, wrangler 4.28.1

### Please provide a link to a minimal reproduction

_No response_

### Describe the Bug

This is on the latest version of wrangler. See the minimal reproduction below:

Worker:

```ts
export default {
async fetch(request, env, ctx): Promise {
const upgradeHeader = request.headers.get('Upgrade');
if (!upgradeHeader || upgradeHeader !== 'websocket') {
return new Response('Expected Upgrade: websocket', {status: 426});
}

const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);

server.accept();

client.addEventListener('close', (e) => {
console.log('WebSocket client connection close requested.');
});

server.addEventListener('close', () => {
console.log('WebSocket server connection close requested.');
});

setInterval(() => {
if (client.readyState === WebSocket.OPEN) {
server.send('Hello from the server!');
} else {
console.log('WebSocket is not open, cannot send message.');
}
}, 1000);

return new Response(null, {
status: 101,
webSocket: client,
});
},

} satisfies ExportedHandler;
```

Client:

```ts
import {WebSocket} from 'ws';

const url = 'ws://127.0.0.1:50616/';

const ws = new WebSocket(url);

ws.on('open', () => {
console.log('Connected to the WebSocket server.');

setTimeout(() => {
console.log("Closing the WebSocket connection after 5 seconds...");
ws.close(4002, 'Normal closure');
}, 3000)
});

ws.on('message', (data) => {
console.log('Message received:', data.toString());
});

ws.on('error', (error) => {
console.error('WebSocket error:', error);
});

ws.on('close', (code, reason) => {
console.log(`WebSocket closed with code: ${code}, reason: ${reason}`);
});
```

**Description of Issue**

I am using, in Production, Cloudflare as a WebSocket proxy.

When you call `ws.close()` on the client side in the above code, nothing happens. Messages continue to be sent, and the socket stays open.

The only way it actually closes, is if on `server.addEventListener('close')`, you listen for the close request, and then call `server.close()`.

When I do this, it closes, but throws an error:

Image

In production, I have over 350,000 error logs in my Dashboard. I've looked at this for hours, and no clear way exists of handling client-initiated disconnects without throwing that error. It's a little painful as far as the developer UX goes.

### Please provide any relevant error logs

```
[InspectorProxyWorker] RUNTIME INCOMING MESSAGE {
method: 'Runtime.exceptionThrown',
params: {
timestamp: 1754834336874,
exceptionDetails: {
exceptionId: 1,
text: 'Uncaught Error: Network connection lost.',
lineNumber: 0,
columnNumber: -2,
url: 'undefined',
executionContextId: 197096833
}
}
}
```

**More Info**

To really hammer this in, take https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server/ for example.

If I take the exact code 1:1, my client code works perfectly fine. The moment I switch `this.ctx.acceptWebSocket(server);` to `server.accept()`, the issue returns.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.