Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebSocket module for Android crashes app when it fails to connect #3346

Closed
yzarubin opened this issue Oct 11, 2015 · 50 comments
Closed

WebSocket module for Android crashes app when it fails to connect #3346

yzarubin opened this issue Oct 11, 2015 · 50 comments
Labels
Resolution: Locked This issue was locked by the bot.

Comments

@yzarubin
Copy link
Contributor

When creating a new WebSocket connection, if the var ws = new WebSocket('....'); call fails to connect to the server, it will crash the app by throwing an error with message Cannot close WebSocket. Unknown WebSocket id 0

This is because the websocketFailed event fires, which calls this._closeWebSocket(id); inside WebSocket.js. Since the connection was never open to begin with, calling WebSocket client = mWebSocketConnections.get(id); inside WebSocketModule.java will return null, hence triggering the exception throw new RuntimeException("Cannot close WebSocket. Unknown WebSocket id " + id);

@ide
Copy link
Contributor

ide commented Oct 11, 2015

Just to confirm, this is on master?

cc @satya164

@ide
Copy link
Contributor

ide commented Oct 11, 2015

Two things we should do: fix the bug and make the native layer uncrashable.

@yzarubin
Copy link
Contributor Author

@ide yes, it is on master. I will submit a 1 line PR to fix this bug in the mean time, as it makes WebSockets unusable on Android at the moment.

@satya164
Copy link
Contributor

@yzarubin Than you.

@ghost ghost closed this as completed in 45644aa Oct 14, 2015
ide pushed a commit to expo/react-native that referenced this issue Oct 23, 2015
…acebook#3346

Summary: Check that the WS state is set to OPEN before trying to close it when the ```websocketFailed``` event fires. Otherwise the app throws an error at the Android level.

Fixes facebook#3346
Closes facebook#3347

Reviewed By: @​svcscm

Differential Revision: D2535807

Pulled By: @mkonicek

fb-gh-sync-id: bb70c551ea2e582cfaa80139a265dbbca6d990d2
MattFoley pushed a commit to skillz/react-native that referenced this issue Nov 9, 2015
…acebook#3346

Summary: Check that the WS state is set to OPEN before trying to close it when the ```websocketFailed``` event fires. Otherwise the app throws an error at the Android level.

Fixes facebook#3346
Closes facebook#3347

Reviewed By: @​svcscm

Differential Revision: D2535807

Pulled By: @mkonicek

fb-gh-sync-id: bb70c551ea2e582cfaa80139a265dbbca6d990d2
Crash-- pushed a commit to Crash--/react-native that referenced this issue Dec 24, 2015
…acebook#3346

Summary: Check that the WS state is set to OPEN before trying to close it when the ```websocketFailed``` event fires. Otherwise the app throws an error at the Android level.

Fixes facebook#3346
Closes facebook#3347

Reviewed By: @​svcscm

Differential Revision: D2535807

Pulled By: @mkonicek

fb-gh-sync-id: bb70c551ea2e582cfaa80139a265dbbca6d990d2
@ThaJay
Copy link

ThaJay commented Sep 4, 2017

The native layer still crashes with no way to prevent it from js. this happens for me when I enable airplane mode after making a websocket connection. I send a keepalive message every few seconds and when it fails because there is no connection I get this error.
RN ~0.42.3

09-04 16:54:00.198 23229 23262 E AndroidRuntime: java.lang.RuntimeException: Cannot send a message. Unknown WebSocket id 2
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at com.facebook.react.modules.websocket.WebSocketModule.send(WebSocketModule.java:221)
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at java.lang.reflect.Method.invoke(Native Method)
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at com.facebook.react.bridge.BaseJavaModule$JavaMethod.invoke(BaseJavaModule.java:345)
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at com.facebook.react.cxxbridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:136)
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at android.os.Handler.handleCallback(Handler.java)
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at android.os.Handler.dispatchMessage(Handler.java)
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:31)
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at android.os.Looper.loop(Looper.java)
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at com.facebook.react.bridge.queue.MessageQueueThreadImpl$3.run(MessageQueueThreadImpl.java:196)
09-04 16:54:00.198 23229 23262 E AndroidRuntime: 	at java.lang.Thread.run(Thread.java:818)
09-04 16:54:00.208  3551  4476 W ActivityManager:   Force finishing activity com.chipta/.MainActivity

I tried a try / catch around the ws.send and also a if this.ws but they both don't prevent this error so I'm kind of stuck. How do I check if I can send a message if the ws object does not give me this information?

edit: It seems possible to prevent this if I check for an internet connection before sending. But it should just raise a js error like a normal http request

@Moumene
Copy link

Moumene commented Oct 23, 2017

@ThaJay i got this error too. People are working with the app outside while they're driving (bad network most of the time)
How can we prevent the crash anyway ? 🤕

@ThaJay
Copy link

ThaJay commented Oct 24, 2017

My comment already states how to prevent the crash:

It seems possible to prevent this if I check for an internet connection before sending. But it should just raise a js error like a normal http request

This should still error in the js and not in the java. Java errors are impossible to deal with from js. So I don't get why this issue was closed. @ide

@danielkcz
Copy link

danielkcz commented Oct 26, 2017

I just run into this issue in the alpha version of our production app. It's not something widespread yet, one user experienced it two times, but I also would like to know why this was closed when it's still an issue (after 2 years of reporting it).

Sadly I don't have any additional information in helping to solve it. Websockets are used inside subscriptions-transport-ws in my case.

image

@RyderMS
Copy link

RyderMS commented Oct 31, 2017

We have hit this as well in our app that is currently in beta, but it is our second most hit crash already. We use websockets in a couple places, and we do the correct hardening in those locations in the javascript. It seems like a bug that the native layer would throw such an exception

@guysegal
Copy link

guysegal commented Nov 2, 2017

We are also experiencing this crash. It happens quite a lot , about 2%-3% of the sessions, to thousands of users.
It seems like this is an ongoing issue, maybe it should be re-open?

@ide
Copy link
Contributor

ide commented Nov 4, 2017

Reopening since this is somewhat core and results in crashes.

@tanthanh289
Copy link

tanthanh289 commented Nov 5, 2017

We have this problem Cannot send a message. Unknown WebSocket id ....
crashes sometimes when app starts.

"react": "16.0.0-alpha.12",
"react-i18next": "5.3.0",
"react-native": "0.48.3",
"socket.io": "2.0.3",
"socket.io-client": "2.0.3",

fabric crashlytics stacktrace attached.
stacktrace.txt

@ghost
Copy link

ghost commented Nov 5, 2017

Same. I get this issue both in debug and production releases on Google Play. I only get this issue on Android.
"react": "^16.0.0",
"react-native": "^0.49.5"

skaermbillede 2017-11-05 kl 12 14 51

@orenklein
Copy link

This issue might be related to #8949

@angrycans
Copy link

************* Crash Log Head ****************
Device Manufacturer: Freescale
Device Model : INBOX310
Android Version : 4.2.2
Android SDK : 17
App VersionName : 0.8.7
App VersionCode : 2
************* Crash Log Head ****************

java.lang.RuntimeException: Cannot send a message. Unknown WebSocket id 0
at com.facebook.react.modules.websocket.WebSocketModule.send(WebSocketModule.java:199)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:363)
at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:162)
at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
at android.os.Handler.handleCallback(Handler.java:725)
at android.os.Handler.dispatchMessage(Handler.java:92)
at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:31)
at android.os.Looper.loop(Looper.java:137)
at com.facebook.react.bridge.queue.MessageQueueThreadImpl$3.run(MessageQueueThreadImpl.java:194)
at java.lang.Thread.run(Thread.java:856)

@RyderMS
Copy link

RyderMS commented Nov 7, 2017

https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.java#L228

It looks to me as if this exception is the cause of the issue, is there a way we can catch this in JS? throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id);

I was thinking of just forking RN and just catching this exception natively just to keep my users from experiencing crashes

@satya164
Copy link
Contributor

satya164 commented Nov 8, 2017

I was thinking of just forking RN and just catching this exception natively

Or just send a PR?

@zxyah
Copy link

zxyah commented Nov 8, 2017

my problem :
in javascript, the socket instance is connected and readyState is OPEN, but when i send a message, android app crash
my solution is copy WebSocketModule.java as a CustomModule

if (client == null) {
            // This is a programmer error
            //throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id);
            mWebSocketConnections.remove(id);
            notifyWebSocketFailed(id, "Unknown WebSocket id");
            return;
        }

@angrycans
Copy link

native close this socket and let react-native js websocket.onerror or onclose
if (client == null) { //close this websocket to js websocket.error }

@joseygordev
Copy link

+1

@cweilguny
Copy link

This really needs to be adressed. Apps using the react native websockets in this state just can't be published, because they are simply broken. You always can check the ready state before sending, but it seems that this isn't really reliable. Then when you socket.send() you get the crash.

@ghost
Copy link

ghost commented Dec 7, 2017

@zxyah Can you please provide an example of how you did it. I have a hard time forking react native and getting that properly installed, so your solution seems best.
It's the most common crash in my app on Android and I need to solve this ASAP

@zxyah
Copy link

zxyah commented Dec 8, 2017

@danieldelouya
React-Native Version:0.47.2
Module:

public class HxSocketModule extends ReactContextBaseJavaModule {
    private final Map<Integer, WebSocket> mWebSocketConnections = new HashMap<>();

    private ReactContext mReactContext;
    private ForwardingCookieHandler mCookieHandler;

    public HxSocketModule(ReactApplicationContext context) {
        super(context);
        mReactContext = context;
        mCookieHandler = new ForwardingCookieHandler(context);
    }

    private void sendEvent(String eventName, WritableMap params) {
        mReactContext
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(eventName, params);
    }

    @Override
    public String getName() {
        return "HxSocket";
    }

    @ReactMethod
    public void connect(
            final String url,
            @Nullable final ReadableArray protocols,
            @Nullable final ReadableMap headers,
            final int id) {
        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read
                .build();

        Request.Builder builder = new Request.Builder()
                .tag(id)
                .url(url);

        String cookie = getCookie(url);
        if (cookie != null) {
            builder.addHeader("Cookie", cookie);
        }

        if (headers != null) {
            ReadableMapKeySetIterator iterator = headers.keySetIterator();

            if (!headers.hasKey("origin")) {
                builder.addHeader("origin", getDefaultOrigin(url));
            }

            while (iterator.hasNextKey()) {
                String key = iterator.nextKey();
                if (ReadableType.String.equals(headers.getType(key))) {
                    builder.addHeader(key, headers.getString(key));
                } else {
                    FLog.w(
                            ReactConstants.TAG,
                            "Ignoring: requested " + key + ", value not a string");
                }
            }
        } else {
            builder.addHeader("origin", getDefaultOrigin(url));
        }

        if (protocols != null && protocols.size() > 0) {
            StringBuilder protocolsValue = new StringBuilder("");
            for (int i = 0; i < protocols.size(); i++) {
                String v = protocols.getString(i).trim();
                if (!v.isEmpty() && !v.contains(",")) {
                    protocolsValue.append(v);
                    protocolsValue.append(",");
                }
            }
            if (protocolsValue.length() > 0) {
                protocolsValue.replace(protocolsValue.length() - 1, protocolsValue.length(), "");
                builder.addHeader("Sec-WebSocket-Protocol", protocolsValue.toString());
            }
        }

        client.newWebSocket(builder.build(), new WebSocketListener() {

            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                mWebSocketConnections.put(id, webSocket);
                WritableMap params = Arguments.createMap();
                params.putInt("id", id);
                sendEvent("websocketOpen", params);
            }

            @Override
            public void onClosed(WebSocket webSocket, int code, String reason) {
                WritableMap params = Arguments.createMap();
                params.putInt("id", id);
                params.putInt("code", code);
                params.putString("reason", reason);
                sendEvent("websocketClosed", params);
            }

            @Override
            public void onFailure(WebSocket webSocket, Throwable t, Response response) {
                notifyWebSocketFailed(id, t.getMessage());
            }

            @Override
            public void onMessage(WebSocket webSocket, String text) {
                WritableMap params = Arguments.createMap();
                params.putInt("id", id);
                params.putString("data", text);
                params.putString("type", "text");
                sendEvent("websocketMessage", params);
            }

            @Override
            public void onMessage(WebSocket webSocket, ByteString bytes) {
                String text = bytes.base64();
                WritableMap params = Arguments.createMap();
                params.putInt("id", id);
                params.putString("data", text);
                params.putString("type", "binary");
                sendEvent("websocketMessage", params);
            }
        });

        // Trigger shutdown of the dispatcher's executor so this process can exit cleanly
        client.dispatcher().executorService().shutdown();
    }

    @ReactMethod
    public void close(int code, String reason, int id) {
        WebSocket client = mWebSocketConnections.get(id);
        if (client == null) {
            // WebSocket is already closed
            // Don't do anything, mirror the behaviour on web
            return;
        }
        try {
            client.close(code, reason);
            mWebSocketConnections.remove(id);
        } catch (Exception e) {
            FLog.e(
                    ReactConstants.TAG,
                    "Could not close WebSocket connection for id " + id,
                    e);
        }
    }

    @ReactMethod
    public void send(String message, int id) {
        WebSocket client = mWebSocketConnections.get(id);
        if (client == null) {
            // This is a programmer error
            //throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id);
            mWebSocketConnections.remove(id);
            notifyWebSocketFailed(id, "hxSocket is null while send");
            return;
        }
        try {
            client.send(message);
        } catch (Exception e) {
            notifyWebSocketFailed(id, e.getMessage());
        }
    }

    @ReactMethod
    public void sendBinary(String base64String, int id) {
        WebSocket client = mWebSocketConnections.get(id);
        if (client == null) {
            // This is a programmer error
            //throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id);
            mWebSocketConnections.remove(id);
            notifyWebSocketFailed(id, "hxSocket is null while send binary");
            return;
        }
        try {
            client.send(ByteString.decodeBase64(base64String));
        } catch (Exception e) {
            notifyWebSocketFailed(id, e.getMessage());
        }
    }

    @ReactMethod
    public void ping(int id) {
        WebSocket client = mWebSocketConnections.get(id);
        if (client == null) {
            // This is a programmer error
            //throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id);
            mWebSocketConnections.remove(id);
            notifyWebSocketFailed(id, "hxSocket is null while ping");
            return;
        }
        try {
            client.send(ByteString.EMPTY);
        } catch (Exception e) {
            notifyWebSocketFailed(id, e.getMessage());
        }
    }

    private void notifyWebSocketFailed(int id, String message) {
        WritableMap params = Arguments.createMap();
        params.putInt("id", id);
        params.putString("message", message);
        sendEvent("websocketFailed", params);
    }

    /**
     * Get the default HTTP(S) origin for a specific WebSocket URI
     *
     * @param uri
     * @return A string of the endpoint converted to HTTP protocol (http[s]://host[:port])
     */

    private static String getDefaultOrigin(String uri) {
        try {
            String defaultOrigin;
            String scheme = "";

            URI requestURI = new URI(uri);
            if (requestURI.getScheme().equals("wss")) {
                scheme += "https";
            } else if (requestURI.getScheme().equals("ws")) {
                scheme += "http";
            } else if (requestURI.getScheme().equals("http") || requestURI.getScheme().equals("https")) {
                scheme += requestURI.getScheme();
            }

            if (requestURI.getPort() != -1) {
                defaultOrigin = String.format(
                        "%s://%s:%s",
                        scheme,
                        requestURI.getHost(),
                        requestURI.getPort());
            } else {
                defaultOrigin = String.format("%s://%s/", scheme, requestURI.getHost());
            }

            return defaultOrigin;
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Unable to set " + uri + " as default origin header");
        }
    }

    /**
     * Get the cookie for a specific domain
     *
     * @param uri
     * @return The cookie header or null if none is set
     */
    private String getCookie(String uri) {
        try {
            URI origin = new URI(getDefaultOrigin(uri));
            Map<String, List<String>> cookieMap = mCookieHandler.get(origin, new HashMap());
            List<String> cookieList = cookieMap.get("Cookie");

            if (cookieList == null || cookieList.isEmpty()) {
                return null;
            }

            return cookieList.get(0);
        } catch (URISyntaxException | IOException e) {
            throw new IllegalArgumentException("Unable to get cookie from " + uri);
        }
    }
}

Package:

public class HxSocketPackage implements ReactPackage {
    // Deprecated RN 0.47
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new HxSocketModule(reactContext));
        return modules;
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

WebSocketEvent.js

/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 */

"use strict";

/**
 * Event object passed to the `onopen`, `onclose`, `onmessage`, `onerror`
 * callbacks of `WebSocket`.
 *
 * The `type` property is "open", "close", "message", "error" respectively.
 *
 * In case of "message", the `data` property contains the incoming data.
 */
class WebSocketEvent {
  constructor(type, eventInitDict) {
    this.type = type.toString();
    Object.assign(this, eventInitDict);
  }
}

module.exports = WebSocketEvent;

WebSocket.js

/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
"use strict";

import { NativeEventEmitter, Platform, NativeModules } from "react-native";

import EventTarget from "event-target-shim";
const base64 = require("base64-js");
const RCTWebSocketModule = NativeModules.HxSocket;
import WebSocketEvent from "./WebSocketEvent";

function binaryToBase64(data) {
  if (data instanceof ArrayBuffer) {
    data = new Uint8Array(data);
  }
  if (data instanceof Uint8Array) {
    return base64.fromByteArray(data);
  }
  if (!ArrayBuffer.isView(data)) {
    throw new Error("data must be ArrayBuffer or typed array");
  }
  const { buffer, byteOffset, byteLength } = data;
  return base64.fromByteArray(new Uint8Array(buffer, byteOffset, byteLength));
}

const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
const CLOSED = 3;

const CLOSE_NORMAL = 1000;

const WEBSOCKET_EVENTS = ["close", "error", "message", "open"];

let nextWebSocketId = 0;

/**
 * Browser-compatible WebSockets implementation.
 *
 * See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
 * See https://github.com/websockets/ws
 */
class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
  static CONNECTING = CONNECTING;
  static OPEN = OPEN;
  static CLOSING = CLOSING;
  static CLOSED = CLOSED;

  CONNECTING: number = CONNECTING;
  OPEN: number = OPEN;
  CLOSING: number = CLOSING;
  CLOSED: number = CLOSED;

  _socketId: number;
  _eventEmitter: NativeEventEmitter;
  _subscriptions: Array;

  onclose: ?Function;
  onerror: ?Function;
  onmessage: ?Function;
  onopen: ?Function;

  binaryType: ?string;
  bufferedAmount: number;
  extension: ?string;
  protocol: ?string;
  readyState: number = CONNECTING;
  url: ?string;

  // This module depends on the native `RCTWebSocketModule` module. If you don't include it,
  // `WebSocket.isAvailable` will return `false`, and WebSocket constructor will throw an error
  static isAvailable: boolean = !!RCTWebSocketModule;

  constructor(
    url: string,
    protocols: ?string | ?Array<string>,
    options: ?{ origin?: string }
  ) {
    super();
    if (typeof protocols === "string") {
      protocols = [protocols];
    }

    if (!Array.isArray(protocols)) {
      protocols = null;
    }

    if (!WebSocket.isAvailable) {
      throw new Error(
        "Cannot initialize WebSocket module. " +
          "Native module RCTWebSocketModule is missing."
      );
    }

    this._eventEmitter = new NativeEventEmitter(RCTWebSocketModule);
    this._socketId = nextWebSocketId++;
    this._registerEvents();
    RCTWebSocketModule.connect(url, protocols, options, this._socketId);
  }

  close(code?: number, reason?: string): void {
    if (this.readyState === this.CLOSING || this.readyState === this.CLOSED) {
      return;
    }

    this.readyState = this.CLOSING;
    this._close(code, reason);
  }

  send(data): void {
    if (this.readyState === this.CONNECTING) {
      throw new Error("INVALID_STATE_ERR");
    }

    if (typeof data === "string") {
      RCTWebSocketModule.send(data, this._socketId);
      return;
    }

    if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
      RCTWebSocketModule.sendBinary(binaryToBase64(data), this._socketId);
      return;
    }

    throw new Error("Unsupported data type");
  }

  ping(): void {
    if (this.readyState === this.CONNECTING) {
      throw new Error("INVALID_STATE_ERR");
    }

    RCTWebSocketModule.ping(this._socketId);
  }

  _close(code?: number, reason?: string): void {
    if (Platform.OS === "android") {
      // See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
      const statusCode = typeof code === "number" ? code : CLOSE_NORMAL;
      const closeReason = typeof reason === "string" ? reason : "";
      RCTWebSocketModule.close(statusCode, closeReason, this._socketId);
    } else {
      RCTWebSocketModule.close(this._socketId);
    }
  }

  _unregisterEvents(): void {
    this._subscriptions.forEach(e => e.remove());
    this._subscriptions = [];
  }

  _registerEvents(): void {
    this._subscriptions = [
      this._eventEmitter.addListener("websocketMessage", ev => {
        if (ev.id !== this._socketId) {
          return;
        }
        this.dispatchEvent(
          new WebSocketEvent("message", {
            data:
              ev.type === "binary"
                ? base64.toByteArray(ev.data).buffer
                : ev.data
          })
        );
      }),
      this._eventEmitter.addListener("websocketOpen", ev => {
        if (ev.id !== this._socketId) {
          return;
        }
        this.readyState = this.OPEN;
        this.dispatchEvent(new WebSocketEvent("open"));
      }),
      this._eventEmitter.addListener("websocketClosed", ev => {
        if (ev.id !== this._socketId) {
          return;
        }
        this.readyState = this.CLOSED;
        this.dispatchEvent(
          new WebSocketEvent("close", {
            code: ev.code,
            reason: ev.reason
          })
        );
        this._unregisterEvents();
        this.close();
      }),
      this._eventEmitter.addListener("websocketFailed", ev => {
        if (ev.id !== this._socketId) {
          return;
        }
        this.readyState = this.CLOSED;
        this.dispatchEvent(
          new WebSocketEvent("error", {
            message: ev.message
          })
        );
        this.dispatchEvent(
          new WebSocketEvent("close", {
            message: ev.message
          })
        );
        this._unregisterEvents();
        this.close();
      })
    ];
  }
}

module.exports = WebSocket;

@ghost
Copy link

ghost commented Dec 8, 2017

@zxyah Thank you :)

@ghost
Copy link

ghost commented Dec 8, 2017

@zxyah Do you happen to know I one can simply edit WebSocketModule.java file in the react-native node_module and right before submitting the app to app store? If this is possible it'll be the easiest way to go for me. I know one would have to edit the file every time you run npm install, but I'm not sure whether it actually stays that way once app is uploaded.

@yedidyak
Copy link
Contributor

yedidyak commented Dec 10, 2017

Here is a WebSocketModule that fixes the issue. Since everything is private it has to be done with reflection.

You need to remove the standard Native Module, and replace it with this:

private class CustomWebSocketModule extends WebSocketModule {

		private Map<Integer, WebSocket> superWebSocketConnections;
		private Method notifyWebSocketFailedMethod;

		CustomWebSocketModule(final ReactApplicationContext context) {
			super(context);
			try {
				Field mWebSocketConnectionsField = getClass().getSuperclass().getDeclaredField("mWebSocketConnections");
				mWebSocketConnectionsField.setAccessible(true);
				this.superWebSocketConnections = (Map<Integer, WebSocket>) mWebSocketConnectionsField.get(this);
				Class[] types = {Integer.TYPE, String.class};
				this.notifyWebSocketFailedMethod = getClass().getSuperclass().getDeclaredMethod("notifyWebSocketFailed", types);
				this.notifyWebSocketFailedMethod.setAccessible(true);
			} catch (NoSuchFieldException e) {
				e.printStackTrace();
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
		}

		@Override
		@ReactMethod
		public void connect(String url, @javax.annotation.Nullable ReadableArray protocols, @javax.annotation.Nullable ReadableMap headers, int id) {
			super.connect(url, protocols, headers, id);
		}

		@Override
		@ReactMethod
		public void close(int code, String reason, int id) {
			super.close(code, reason, id);
		}

		@Override
		@ReactMethod
		public void sendBinary(String base64String, int id) {
			super.sendBinary(base64String, id);
		}

		@Override
		@ReactMethod
		public void ping(int id) {
			super.ping(id);
		}

		@Override
		@ReactMethod
		public void send(String message, int id) {
			try {
				WebSocket client = this.superWebSocketConnections.get(id);
				if (client == null) {
					this.superWebSocketConnections.remove(id);
					this.notifyWebSocketFailedMethod.invoke(this, id, "Unknown WebSocket id");
					return;
				} else {
					super.send(message, id);
				}
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
		}
	} 

@tanthanh289
Copy link

tanthanh289 commented Dec 20, 2017

I have found one of problems is due to HashMap used for mWebSocketConnections in WebSocketModule.java.

HashMap is not thread-safe, and can lose socket clients if concurrent sockets are opened at the same time.

If you can build RN from android source, just try this:

import java.util.concurrent.ConcurrentHashMap;

// change from HashMap to ConcurrentHashMap
private final Map<Integer, WebSocket> mWebSocketConnections = new ConcurrentHashMap<>();
private final Map<Integer, ContentHandler> mContentHandlers = new ConcurrentHashMap<>();

// and more safety:
  @ReactMethod
  public void send(String message, int id) {
    WebSocket client = mWebSocketConnections.get(id);
    if (client == null) {
      // This is a programmer error
      // throw new RuntimeException("Cannot send a message. Unknown WebSocket id " + id);

        // just notify error
        WritableMap params = Arguments.createMap();
	    params.putInt("id", id);
	    params.putString("message", "client is null");
	    sendEvent("websocketFailed", params);
	    params = Arguments.createMap();
	    params.putInt("id", id);
	    params.putInt("code", 0);
	    params.putString("reason", "client is null");
	    sendEvent("websocketClosed", params);
	    mWebSocketConnections.remove(id);
	    mContentHandlers.remove(id);
        return;
    }
    try {
      client.send(message);
    } catch (Exception e) {
      notifyWebSocketFailed(id, e.getMessage());
    }
  }

@cweilguny
Copy link

That sounds more like a solution than just changing the way how the error is thrown/bubbled down to JS. My experiments also showed, that often the connection shouldn't have gone, but the runtime exception is thrown. So dispatching an error event in those cases would only move the bug to JS where it can't be solved but just workarounded. It also occours, that the close event is dispatched before the open event, this situation also leads to the point where the uncatchable runtime exception is thrown.

@Jose4gg
Copy link

Jose4gg commented Jan 8, 2018

+1 Any updates in this one?

@satya164
Copy link
Contributor

satya164 commented Jan 9, 2018

@tanthanh289 can you send a pull request please? it takes very few minutes to send a PR with your changes if you have already fixed it and helps everyone.

@padphp
Copy link

padphp commented Jan 23, 2018

+1

1 similar comment
@Hyllesen
Copy link

+1

@satya164
Copy link
Contributor

Please stop commenting with +1. It sends a notification to everyone in the thread and doesn't achieve anything to resolve the issue.

Many people have commented here with the fix, (especially @tanthanh289) and it'll be useful if someone sends a pull request with that change to fix it. If this issue affects you, please send a pull request and mention me, and I'll try to get it merged.

@sunweiyang
Copy link
Contributor

@satya164, I've created PR #17884 to address this issue. Would like to greatly credit @tanthanh289 for inspiring this fix.

facebook-github-bot pushed a commit that referenced this issue Feb 9, 2018
Summary:
… prevent unknown websocket IDs from crashing on Android (show warning on development builds instead)

This PR addresses #3346; an unknown websocket ID should produce a warning during development, but not cause crashes in production RN apps. This PR was created by satya164's request, and was inspired by tanthanh289's suggestion on #3346's thread.

On Android, create a websocket using a service like Pusher (`pusher-js` npm package) or manually, and then induce removal of its websocket ID. Result should be a red warning screen during development, and no crash in the app's release variant.

 [ANDROID] [BUGFIX] [WebSocket] - Prevent unknown websocket IDs from crashing on Android
Closes #17884

Differential Revision: D6954038

Pulled By: hramos

fbshipit-source-id: b346d80d7568996b8819c0de54552abb534cbfae
Plo4ox pushed a commit to Plo4ox/react-native that referenced this issue Feb 17, 2018
Summary:
… prevent unknown websocket IDs from crashing on Android (show warning on development builds instead)

This PR addresses facebook#3346; an unknown websocket ID should produce a warning during development, but not cause crashes in production RN apps. This PR was created by satya164's request, and was inspired by tanthanh289's suggestion on facebook#3346's thread.

On Android, create a websocket using a service like Pusher (`pusher-js` npm package) or manually, and then induce removal of its websocket ID. Result should be a red warning screen during development, and no crash in the app's release variant.

 [ANDROID] [BUGFIX] [WebSocket] - Prevent unknown websocket IDs from crashing on Android
Closes facebook#17884

Differential Revision: D6954038

Pulled By: hramos

fbshipit-source-id: b346d80d7568996b8819c0de54552abb534cbfae
@sharmahanish17
Copy link

Hello all,

I am also facing the same issue the app is running fine in iOS but getting crashed in Android. Few days back it was working fine, but suddenly I started getting this issue. Please assist me.

Thanks

@satya164
Copy link
Contributor

Closing this since the PR has been merged already.

@jmsbrett
Copy link

Has anyone seen this error before

"websocketFailed",{"message":"unexpected end of stream on Connection{10.0.2.2:8080, proxy=DIRECT hostAddress=/10.0.2.2:8080 cipherSuite=none protocol=http/1.1}","id":3

@vygandas
Copy link

+1

@s349856186
Copy link

Has anyone seen this error before

"websocketFailed",{"message":"unexpected end of stream on Connection{10.0.2.2:8080, proxy=DIRECT hostAddress=/10.0.2.2:8080 cipherSuite=none protocol=http/1.1}","id":3

Do you solve it?How to do? @jmsbrett

@jmsbrett
Copy link

@s349856186

I did not solve it.

@alexlevy0
Copy link

+1

@AdamZaczek
Copy link

It's still happening

@cbowesntu
Copy link

+1 - ios as well

@facebook facebook locked as resolved and limited conversation to collaborators Dec 11, 2019
@react-native-bot react-native-bot added the Resolution: Locked This issue was locked by the bot. label Dec 11, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Resolution: Locked This issue was locked by the bot.
Projects
None yet