yjs / y-websocket

Websocket Connector for Yjs
https://docs.yjs.dev/ecosystem/connection-provider/y-websocket
MIT License
526 stars 263 forks source link

iOS/iPadOS/Safari MacOS does not detect loss of websocket connectivity after network change causing data loss #139

Open rcbevans opened 1 year ago

rcbevans commented 1 year ago

Checklist

Describe the bug There is an open bug in Webkit in which it doesn't raise the correct onerror/onclose handlers when the network is lost/changes. y-websocket does not detect a loss of connectivity and therefore doesn't handle it appropriately. This means the client doesn't know that it is offline and all updates are fired off into the abyss causing user data loss without local persistence.

https://bugs.webkit.org/show_bug.cgi?id=247943 https://stackoverflow.com/questions/75869629/ios-websocket-close-and-error-events-not-firing

To Reproduce Steps to reproduce the behavior:

  1. Create a y-websocket connection on an iPhone when using Wi-Fi.
  2. Turn off Wi-Fi so network connectivity switches back to cellular.
  3. No onerror/onclose is raised by webkit and subsequently, by the websocket provider, so the client is unaware it is offline and any changes sent are lost.
  4. The connection never recovers, so even though other network calls will succeed, any y-doc changes will be lost.

Expected behavior y-websocket should detect that connectivity has been lost and raise the appropriate connection lifecycle events so the user can be notified, and the underlying websocket recreated to re-establish a connection.

Environment Information

Additional context This is not a y-websocket issue but an underlying issue in Webkit; but it is a pretty devastating bug for any products using it since user data is lost without any warning.

I was forced to implement a custom heartbeat in my websocket packets and detect loss of connectivity so I could manually recreate the websocket to reestablish a connection. (There is also an issue here, since calling provider.close() does not clean up internal state because it relies on _ws.onclose firing to do clean up, so it was necessary to manually clean up internal state between provider.close() and provider.connection().

I'm not sure if this is something y-websocket could/should try and handle itself, but I wanted to file an issue so anyone else using the library and investigating reports of iDevice data loss could hopefully see and workaround the problem since Apple are taking their time to address the root cause.

grootgordon commented 1 year ago

I got the same error when use iOS15.0 WebView and iOS16.4.1 WebView. UserAgent as follow Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148') Mozilla/5.0 (iPhone; CPU iPhone OS 16_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148

but Its ok in iOS14.0.3(safari 14.4.2) Webview.

I have tried to use ua-parser-js npm library to distinguish different os and webkit version, and emit close event by manual, the code snippet as follow:

import { UAParser } from 'ua-parser-js'; 
export class HocuspocusProviderWebsocket extends EventEmitter {

clientUA = new UAParser(window.navigator.userAgent)
  getEngineName(): string {
    return this.clientUA.getEngine().name?.toLowerCase() ?? '';
  }

  getOSName(): string{
    const os = this.clientUA.getOS();
    return os.name?.toLowerCase() ?? ''
  }

  getOSVersion(): number {
    const os = this.clientUA.getOS();
    return parseFloat(os.version ?? '0');
  }
}
....

if (
      this.getEngineName() === 'webkit' &&
      this.getOSName() === 'ios' &&
      this.getOSVersion() >= 15.0
    ) {

      const payload = {
        code: 4410,
        reason: 'Client Connection Timeout',
      }

      this.emit("close", { event: payload })
    } 

the result is: iOS 15.0 is fine when turn off wifi, it can switch to cellular automatically. but iOS16 still hang and cannot receive 'close' event, that's very confusing :-(