WebSockex - An Elixir WebSocket client

Hi @Azolo!

I wonder if it’s possible to use WebSockex with subprotocols? I can’t seem to find any mention of it neither in the docs nor in code …

EDIT:

WebSockex.start_link(url, __MODULE__, state, [
  name: __MODULE__, extra_headers: [{"Sec-WebSocket-Protocol", "my-protocol"}]
])

seems to work fine.

It seems like websockex doesn’t support iodata in send_frame?

** (exit) exited in: WebSockex.call(Some.Client, {:text, ["{\"cmd\":", [34, [], "keepalive", 34], ",\"session_id\":", [34, [], 34], ",\"transaction\":", [34, [], "y653PDXvxmGaW+0y4Nab", 34], 125]})
    ** (EXIT) an exception was raised:
        ** (ArgumentError) argument error
            :erlang.byte_size(["{\"cmd\":", [34, [], "keepalive", 34], ",\"session_id\":", [34, [], 34], ",\"transaction\":", [34, [], "y653PDXvxmGaW+0y4Nab", 34], 125])
            (websockex) lib/websockex/frame.ex:296: WebSockex.Frame.get_payload_length_bin/1
            (websockex) lib/websockex/frame.ex:257: WebSockex.Frame.encode_frame/1
            (websockex) lib/websockex.ex:723: WebSockex.sync_send/5
            (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
    (websockex) lib/websockex.ex:349: WebSockex.send_frame/2

Wouldn’t it be easier to use IO.iodata_length instead of :erlang.byte_size? Since it works both on binaries and iolists.

iex(8)> data = API.Keepalive.new() |> Jason.encode_to_iodata!() |> IO.iodata_length()
74
iex(9)> data = API.Keepalive.new() |> Jason.encode!() |> IO.iodata_length()
74
1 Like

Honestly, it probably is easier to use IO.iodata_length. Feel free to open an issue or PR.

The thing that I don’t know about is checking to see if iodata is valid UTF8 for text frames. Currently I’m using String.valid?/1. If I remember correctly it doesn’t work with iodata. If it does, then I am totally wrong and there is no problem at all.

The thing that I don’t know about is checking to see if iodata is valid UTF8 for text frames. Currently I’m using String.valid?/1. If I remember correctly it doesn’t work with iodata. If it does, then I am totally wrong and there is no problem at all.

Oh, I haven’t thought about it. Is this check required by the spec?

If it is, something like cowlib/src/cow_ws.erl at master · ninenines/cowlib · GitHub can be adapted to work with iodata.

The validation in cowboy though happens only on the receiving end, it seems.

Section 5.6 of the WebSocket Spec says that all complete text data frames need to be valid UTF8. However, closer reading of Section 8.1 implies that you could get away with sending a non-valid text frame and it is on the receiver to verify.

I think for the sake of this being a library and such, it should just work with as many use cases as possible instead of being the most correct version possible. So I am more than willing to forego the outgoing check in favor of making it work for your use case. Especially since the spec implies I can do that.

But there are probably a couple of non-trivial changes to make for iodata. Unless I don’t care about the overall send/write performance and just set it as a binary before it was sent, which would defeat the point of iodata.

WebSockex 0.4.1 Released!

Enhancements

  • Allow :via and :global tuples for named registration.
    • This includes handling for cast/2 and send_frame/2.
  • Add access to response headers during handle_connect/2 via Conn.resp_headers.
  • Add Conn.parse_url/1 to handle url to URI conversion.
  • Allow Conn.new/2 to use a url string instead of a URI struct.
  • Automatically add a “/” path to a pathless url.
    • The HTTP request will break without a valid path!
  • Add child_spec definitions for Elixir 1.5+
    • Or any version that exports Supervisor.child_spec/2
  • Some documentation tweaks

Bug Fixes

  • No longer invoke handle_disconnect if there is reason to exit from invoking a callback. (e.g. an exception was raised)
  • Properly handle unexpected SSL socket termination.
    • This seems pretty important, but I don’t know…
  • Return a descriptive error when trying to use send_frame/2 in a callback.

Take a look at the v0.4.0...v0.4.1 diff or the release on hex.pm for more information.

5 Likes

Thumbs up for sharing this work. I’m using it to integrate with Slack RTM.

Cannot use proxy. When will proxy option be added?

This is probably a basic question, but I’ve been having some problems getting modules where I use WebSockex to not crash their supervisors when there’s an issue connecting (e.g. when I turn my wifi off).

Here’s my start_link:

  def start_link(opts \\ []) do
    state = %{heartbeat: 0}

    socket_opts = [
      ssl_options: [
        ciphers: :ssl.cipher_suites() ++ [{:rsa, :aes_128_cbc, :sha}]
      ],
      handle_initial_conn_failure: true
    ]

    opts = Keyword.merge(opts, socket_opts)
    WebSockex.start_link(@socket_url, __MODULE__, state, opts)
  end

With my wifi off, iex -S mix will crash with:

* (Mix) Could not start application app: App.Application.start(:normal, []) returned an error: shutdown: failed to start child: App.Sockets.Supervisor
    ** (EXIT) shutdown: failed to start child: App.Sockets.Foo
        ** (EXIT) %WebSockex.ConnError{original: :timeout}

I’ve implemented a handle_disconnect/2. What changes should I make so that a networking failure doesn’t crash the whole module (and its supervisors)?

1 Like

Maybe you need to implement terminate/2?

Terminating with :normal after an Exceptional Close or Error

Usually you’ll want to negotiate and handle any abnormal close event or error leading to it, as per WS Spec, but there might be cases where you simply want the socket to exit as if it was a normal event, even if it was abruptly closed or another exception was raised. In those cases you can define the terminate callback and return exit(:normal) from it.

def terminate(reason, state) do 
  IO.puts(\nSocket Terminating:\n#{inspect reason}\n\n#{inspect state}\n") 
   exit(:normal) 
end

I’ve implemented a terminate very similar to that one. It gets executed if I cut the web connection after connecting but not if the connection is off when start_link() is called. In both cases, this leads to taking down the whole process and its supervisors.

The behavior I want instead is to just keep trying to reconnect to the 3rd party endpoint periodically.

2 Likes

Stumbled on the same problems.
So basically I created a GenServer which is wrapper for the WebSockex process.
Starting the GenServer will return the pid no matter if the WebSockex fails on the start_link or not. You actualy start the websocket connection in the handle_continue/2 func asynchrously.
Then you need to specify handle_initial_conn_failure: true option on the WebSockex start_link func so you can handle the error in the handle_disconnect/2 func. In the handle_disconnect/2 func you can return the tuple {:reconnect, state} for automatic reconnecting. I have added some process sleep there aswell. Here’s the code:

      def start_link(args) do
        GenServer.start_link(__MODULE__, args)
      end

      @impl GenServer
      def init(state) do
        {:ok, state, {:continue, :start_ws}}
      end

      @impl GenServer
      def handle_continue(:start_ws, state) do
        {:ok, pid} = WebSockex.start_link(url, __MODULE__, [], [handle_initial_conn_failure: true])

        subscribe(pid, state)

        {:noreply, state}
      end

      @impl WebSockex
      def handle_disconnect(_conn, state) do
        IO.puts("Disconnected!")

        # Wait before trying to reconnect.
        Process.sleep(5000)

        {:reconnect, state}
      end

Theres also an option called async on the WebSocex library but this does not seem to work with the sending frame or I am missing something.

1 Like