Phoenix channel 'onError((e)' errors

Hi Everyone,

I am making this multiplayer online game playable via the browser and its heavily dependent on websockets.

I am using phoenix channels at the backend and react js as the front end.

I am facing this issue where the channel connection often suffers from errors for unknown reasons.
The onError callback for phoenix channels js client is executed often around every 5~10min. I am not sure why this happens.

Currently, I am doing nothing in the onError callback just logging to console hopping that it will reconnect.

      gameChannel.onError((e) => {
        console.log('An error occured on game channel ', e);
      });

however this error leads to issues in the UI since on the server I have some logic which executes when player disconnects.

I have the backend deployed on gigalixir, there are no errors on the backend channel. This problem also happens some times when I run the backend server locally so I am not sure wether its due to gigalixir.

Can someone help me on why this happens, possible cause behind the on error callback getting triggered and any fixes?

My assumption is it happens maybe because the browser drops the websocket connection occasionally due to some reason, maybe when the user has minimized the browser or its not in focus or due to some issue with react, the socket and channel is stored in a react state.

Its pretty random and hard to debug the root cause, any help would be great, this is the last issue that I need to fix everything else seems to work.

Are you setting the server timeout to a small number anyway?. Like 5s for example here

socket "/socket", EnterpriseWeb.UserSocket,
    websocket: [
      timeout: 5_000
    ],
    longpoll: false

Thanks for replying.

I don’t have any timeout option specified in the websocket configuration.
This is what I have

  socket "/socket", PictionaryWeb.UserSocket,
    websocket: true,
    longpoll: false

The docs say

  • :timeout - the timeout for keeping websocket connections open after it last received data, defaults to 60_000ms

How do You connect?

This is the javscript code that I have to connect to the socket and channel

const socket = new Socket(WEBSOCKET_API, { params: { token } });
socket.connect();

Later, in the code

const gameChannel = socket.channel(`game:${gameId}`, {});
 gameChannel.join()
        .receive('ok', (payload) => {
          if (Object.keys(payload).length !== 0) emitter({ type: SET_GAMEPLAY_STATE, payload });
          emitter({ type: HANDLE_GAME_JOIN_SUCCESS });
        })
        .receive('error', resp => emitter({ type: HANDLE_GAME_JOIN_FAIL, payload: resp.reason }))
        .receive('timeout', () => emitter({ type: HANDLE_GAME_JOIN_FAIL, payload: 'Could not join game in time' }));

      setupGameChannelPresenceHandlers(gameChannel, emitter);
      setupGameChannelEventHandlers(gameChannel, emitter);

      gameChannel.onError((e) => {
        console.log('An error occured on game channel ', e); // I can see this log randomly in the console, and I suspect this is the problem
      });

      gameChannel.onClose((e) => {
        if (e.code === 1005) {
          console.log('WebSocket: closed');
          // Terminate watcher saga watcher saga by sending END
          emitter(END);
        } else {
          console.log('Socket is closed Unexpectedly', e);
          emitter({ type: ADD_ALERT, alertType: 'error', msg: 'Socket is closed Unexpectedly' });
        }
      });

I am using redux-saga so the code has references like emitter.

I have something similar, but not with redux saga…

I like to pass logger option for socket.

    {
      params: { token },
      logger: (kind, msg, data) => (
        // eslint-disable-next-line no-console
        console.log(`${kind}: ${msg}`, data)
      ),
    };

Another difference is how I connect my socket.

  const openSocket = (options = {}) => {
    const newSocket = new Socket(SOCKET_URL, options);
    newSocket.connect();

    // This will also be triggered when socket reconnect
    newSocket.onOpen(() => dispatch({ type: SOCKET_CONNECTED, payload: newSocket }));
    newSocket.onError(() => dispatch({ type: SOCKET_ERROR }));
    newSocket.onClose(() => dispatch({ type: SOCKET_CLOSED }));
    return newSocket;
  };

And this is how I connect my channels.

  const _buildChannel = (topic, receive, callback) => {
    const channel = state.socket.channel(topic, {});

    // Create channels events from a list
    if (receive) {
      receive.forEach(type => 
        channel.on(type, payload => callback({topic, type, payload}))
      );
    }

    channel.join()
      .receive('ok', () =>
        dispatch({ type: CHANNEL_CONNECTED, payload: { topic, channel } }))
      .receive('error', ({reason}) =>
        dispatch({ type: CONNECT_CHANNEL_ERROR, payload: { topic, error: reason } }))
      .receive('timeout', () => {
        // eslint-disable-next-line no-console
        console.log('Networking issue. Still waiting...');
        dispatch({ type: CONNECT_CHANNEL_TIMEOUT, payload: { topic, error: 'Networking issue. Still waiting...' } });
      }); 

    channel.onError(() => 
      dispatch({ type: CHANNEL_ERROR, payload: { topic, error: 'there was an error!' } })
    );
    channel.onClose(() => {
      dispatch({ type: CHANNEL_CLOSED, payload: { topic, error: 'the channel has gone away gracefully' } })
    });

    return channel;
  }

It’s a custom react hook I wrote, to help connecting. socket and channels are also in state (useReducer state). I cannot help for redux saga, but I don’t have issues like You mention.

I tried passing the logger option when creating the socket, I also set the socket time out to infinity in pheonix

  socket "/socket", PictionaryWeb.UserSocket,
    websocket: [timeout: :infinity],
    longpoll: false

I can still see logs in the browser, about errors

These are logs from the logger I passed when creating the socket connection.

image

As you can see it closes the connection due to some unknown error and then reconnects again.