iOS - Websocket close and error events not firing
Asked Answered
N

2

7

We are noticing issues with Safari iOS not calling Websocket events when the Websocket connection is lost. Our web application has no clue the Websocket's connection has been lost. On Android devices, as soon as the connection is severed, the close and error Websocket events are fired.

We created a quick example.

Websocket server in nodeJS

const WebSocket = require('ws');
const wss = new WebSocket.Server({port: 8080});
wss.on('connection', function connection(ws) {
    ws.on('message', function incoming(message) {
        ws.send(`You sent: ${message}`);
    });
    ws.on('close', function close() {
        console.log('Client has disconnected');
    });
});

Simple client

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Example</title>
</head>
<body>
<h1>WebSocket Example</h1>
<div id="output"></div>
<form>
    <label>
        Message:
        <input type="text" id="message">
    </label>
    <button type="submit" id="send">Send</button>
</form>
<script>
    const output = document.getElementById('output');
    const messageInput = document.getElementById('message');
    const sendButton = document.getElementById('send');
    const ws = new WebSocket('ws://localhost:8080');
       
    ws.addEventListener('open', function (event) {
        console.log((new Date()).toISOString(), '********************** OPEN **********************');
    });

    ws.addEventListener('close', function (event) {
        console.log((new Date()).toISOString(), '********************** CLOSE **********************');
    });

    ws.addEventListener('error', function (event) {
        console.log((new Date()).toISOString(), '********************** ERROR **********************');
    });

    ws.addEventListener('message', function (event) {
        console.log((new Date()).toISOString(), '********************** MESSAGE **********************');

        // Append the message to the output div
        const message = document.createElement('p');
        message.textContent = event.data;
        output.appendChild(message);
    });

    sendButton.addEventListener('click', function (event) {
        event.preventDefault();

        const message = messageInput.value;

        // Send the message to the server
        ws.send(message);
    });
</script>
</body>
</html>

When the above code runs, iOS mobile Safari does not fire the events close or error when the Websocket connection is closed.

Examples of closing the Websocket are:

  • Putting device on airplane mode
  • Powering off wifi router
  • Turning device's wifi off

As mentioned before this works fine on Android and other devices, only iOS Safari behaves this way, has anyone ever encountered this with their Web Applications?

EDIT 2023 05 18:

There is a Webkit bug reported related to this behavior here: https://bugs.webkit.org/show_bug.cgi?id=247943

A temp workaround would be to handle the window.onoffline event to warn users etc. But, hopefully they can fix this soon.

Noncombatant answered 28/3, 2023 at 18:5 Comment(5)
I am also looking for this solution. This sucks on mobile ios and mac os safari.Laissezfaire
It does, even more that we are now seing this behavior in Chrome, not only iOS SafariNoncombatant
Did you dig into this any further and find any workarounds? This is impacting users of my y-websocket based application and causing perceived data loss. As soon as there is any kind of network switch, the client disconnects from the collaborative/persistence backend, which is find, it should all sync up on reconnect, but the client never realises it's offline and continues firing edits into the void which never reach the backend, and no offline warning is displayed. Once the network reconnects, the websocket never reestablishes so all data since network change is lost.Illfounded
@Illfounded I updated my post with a webkit bug link. lets hope they fix this soon.Noncombatant
window.onoffline is a nice trick but in my testing I found that if the connection lapses for more than ~15 seconds, it never recovers and no event like onerror is raised leaving a zombie connection I don't know is dead. I ended up implementing my own heartbeat protocol over the websocket to detect the dropped connection and force recreateIllfounded
D
3

I find a early issue https://bugs.chromium.org/p/chromium/issues/detail?id=197841 . The point is close event will delayed triggering and the delay time depend on OS , browser and others. In my environment, close event was triggered 12min after shutting down wifi. I tried two methods to fix it:

  1. add heartbeat , refer to https://github.com/websockets/ws/issues/1158#issuecomment-311321579

  2. use window.addEventListener('offline',callback) , do something in callback when disconnect

Drucilladrucy answered 7/6, 2023 at 7:52 Comment(0)
A
1

For me the workaround was to instead use SSE, combined with normal HTTP POST/PUT endpoints for client-to-server-commands. When I rewrote the code from WebSockets to SSE, it works perfectly and handles all scenarios: internet connection lost, device lock/sleep, minimising Safari. It automatically recovers the connection without any special code.

const eventSource = new EventSource('/sse');
Alasteir answered 12/7, 2023 at 9:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.