cloudflare / cloudflare/workerd
WebSocket API (WebSocketPair) Disconnect Logic Broken
- 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:
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
Assessment
This issue has not been assessed yet.