Firefox fires both error and close events on websocket connection loss
Categories
(Core :: DOM: Networking, defect, P4)
Tracking
()
People
(Reporter: saschanaz, Unassigned)
References
Details
(Whiteboard: [necko-triaged])
Copypasting bug 1944288.
- Set up a local Deno server with the following script (sends websocket message every second):
Deno.serve((req) => {
if (req.headers.get("upgrade") != "websocket") {
return new Response(`
<!DOCTYPE html>
<div id="log"></div>
<script>
const onmessage = () => {
log.prepend(document.createElement("br"))
log.prepend(Date.now())
}
const setup = () => {
log.prepend(document.createElement("br"))
log.prepend(Date.now() + " connecting websocket");
socket = new WebSocket("/");
socket.onmessage = onmessage;
socket.onclose = setup;
socket.onerror = setup;
}
setup();
</script>
`, {
headers: {
"content-type": "text/html"
}
});
}
const userAgent = req.headers.get("user-agent")
const { socket, response } = Deno.upgradeWebSocket(req);
socket.addEventListener("open", () => {
console.log(`a client connected! ${userAgent}`);
});
const handle = setInterval(() => {
socket.send("ping");
}, 1000)
socket.addEventListener("close", () => {
console.log(`a client disconnected! ${userAgent}`)
clearTimeout(handle);
});
return response;
});
- Access the page on Firefox for Android and Chrome for Android
- Either turn off the screen or switch to other apps (e.g. start Slacking 🙂)
On Chrome: close event fires
On Firefox: both events fire
Comment 1•4 months ago
|
||
I believe Firefox's behaviour is also correct, as a close will always follow an error
https://websockets.spec.whatwg.org/#eventdef-websocket-error
When the WebSocket connection is closed, possibly cleanly, the user agent must queue a task to run the following substeps:
Change the ready state to CLOSED (3).
If the user agent was required to fail the WebSocket connection, or if the WebSocket connection was closed after being flagged as full, fire an event named error at the WebSocket object. [WSP]
Fire an event named close at the WebSocket object, using CloseEvent, with the wasClean attribute initialized to true if the connection closed cleanly and false otherwise, the code attribute initialized to the WebSocket connection close code, and the reason attribute initialized to the result of applying UTF-8 decode without BOM to the WebSocket connection close reason. [WSP]
But I've not worked closely very closely with WebSockets.
| Reporter | ||
Comment 2•4 months ago
•
|
||
That looks like the spec actually requires to fire both. Or someone forget to return on step 2. 🤔
Comment 3•4 months ago
|
||
So perhaps one should file an issue on the spec, or add a WPT that checks for getting both events. It's an old spec, so changing it likely will have little impact, unless this behavior is breaking something (which it could be if Chrome only sends one, for example)
Updated•4 months ago
|
| Reporter | ||
Comment 4•2 months ago
|
||
Tested this again, while serving the server in device A and connecting to the server in device B. And disconnect from wifi on device B.
On Android:
- Chrome fires only close
- Firefox fires both
On Windows:
- Chrome fires only close, after a long delay
- Firefox fires both, also after a long delay
On macOS:
- Safari immediately fires both events
- Firefox fires both too after a bit of delay
- Chrome fires only close event after a long delay
Probably a Chrome bug. I'll file a bug there.
| Reporter | ||
Comment 5•2 months ago
|
||
The test script for comment #4:
Deno.serve((req) => {
if (req.headers.get("upgrade") != "websocket") {
return new Response(
`
<!DOCTYPE html>
<div id="log"></div>
<script>
const onmessage = (message = "") => {
log.prepend(document.createElement("br"))
log.prepend(message + " " + Date.now())
}
const setup = () => {
log.prepend(document.createElement("br"))
log.prepend(Date.now() + " connecting websocket");
socket = new WebSocket("/");
socket.onmessage = onmessage;
socket.onclose = () => onmessage("closed");
socket.onerror = () => onmessage("error");
}
setup();
</script>
`,
{
headers: {
"content-type": "text/html",
},
},
);
}
const userAgent = req.headers.get("user-agent");
const { socket, response } = Deno.upgradeWebSocket(req);
socket.addEventListener("open", () => {
console.log(`a client connected! ${userAgent}`);
});
const handle = setInterval(() => {
socket.send("ping");
}, 1000);
socket.addEventListener("close", () => {
console.log(`a client disconnected! ${userAgent}`);
clearTimeout(handle);
});
return response;
});
| Reporter | ||
Updated•2 months ago
|
Description
•